Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions apps/client/src/common/api/rundown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,17 @@ export async function fetchCurrentRundown(options?: RequestOptions): Promise<Run
throw new Error('Invalid rundown payload');
}
return res.data;
}

function isValidRundown(x: any): x is Rundown {
return (
x &&
typeof x === 'object' &&
typeof x.id === 'string' &&
Array.isArray(x.order) &&
Array.isArray(x.flatOrder) &&
x.entries &&
typeof x.entries === 'object' &&
typeof x.revision === 'number'
);
/**
* HTTP request to fetch all entries in the given rundown
*/
export async function fetchRundown(rundownId: RundownId, options?: RequestOptions): Promise<Rundown> {
const res = await axios.get(`${rundownPath}/${rundownId}`, { signal: options?.signal });
if (!isValidRundown(res.data)) {
throw new Error('Invalid rundown payload');
}
return res.data;
}

/**
Expand Down Expand Up @@ -197,3 +195,16 @@ export async function requestDeleteAll(rundownId: RundownId): Promise<AxiosRespo
}

// #endregion operations on rundown entries =======================

function isValidRundown(x: any): x is Rundown {
return (
x &&
typeof x === 'object' &&
typeof x.id === 'string' &&
Array.isArray(x.order) &&
Array.isArray(x.flatOrder) &&
x.entries &&
typeof x.entries === 'object' &&
typeof x.revision === 'number'
);
}
1 change: 1 addition & 0 deletions apps/client/src/common/hooks-query/useProjectRundowns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
renameRundown,
} from '../api/rundown';

//TODO: make suspends so we don't have to deal with no value all over
/**
* Project rundowns
*/
Expand Down
28 changes: 25 additions & 3 deletions apps/client/src/common/hooks-query/useRundown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react';

import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
import { fetchCurrentRundown } from '../api/rundown';
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
import { useSelectedEventId } from '../hooks/useSocket';
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
import { useProjectRundowns } from './useProjectRundowns';
Expand All @@ -20,7 +20,11 @@ const cachedRundownPlaceholder: Rundown = {
};

/**
* Normalised rundown data
* Normalised rundown data for the currently loaded rundown.
*
* Bootstraps via the `/current` alias so the first paint is a single round-trip,
* independent of the project rundown list. Once the loaded id is known, the
* query key swaps to the id-keyed cache that is shared with `useRundownById`.
*/
export default function useRundown() {
const queryClient = useQueryClient();
Expand All @@ -34,7 +38,7 @@ export default function useRundown() {
refetchInterval: queryRefetchIntervalSlow,
});

// Seed the ID-based cache when fetching via the 'current' alias (bootstrap)
// Seed the id-keyed cache when fetching via the bootstrap alias
useEffect(() => {
if (!data || loadedRundownId) return;
queryClient.setQueryData(getRundownQueryKey(data.id), data);
Expand Down Expand Up @@ -115,3 +119,21 @@ export function useRundownAuxData() {
}, [data]);
return { data: filteredData, status };
}

/**
* Provides access to a specific rundown by ID.
* When rundownId is null/undefined the query is disabled and returns the placeholder.
*/
export function useRundownById(rundownId: string | null | undefined) {
const enabled = Boolean(rundownId);

const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
queryKey: getRundownQueryKey(rundownId ?? ''),
queryFn: ({ signal }) => fetchRundown(rundownId!, { signal }),
enabled,
placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchIntervalSlow,
});

return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
}
54 changes: 54 additions & 0 deletions apps/client/src/common/hooks-query/useScopedRundown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { EntryId, Rundown } from 'ontime-types';
import { useMemo } from 'react';

import { useSelectedEventId } from '../hooks/useSocket';
import { getFlatRundownMetadata, type ExtendedEntry } from '../utils/rundownMetadata';
import { useProjectRundowns } from './useProjectRundowns';
import { useRundownById } from './useRundown';

export type RundownSource = {
rundownId: string | null;
rundown: Rundown;
flatRundown: ExtendedEntry[];
status: string;
selectedEventId: EntryId | null;
};

/**
* Explicitly scoped rundown data for views that may operate on a non-loaded rundown.
*/
export function useScopedRundown(rundownId: string | null): RundownSource {
const { data: projectRundowns } = useProjectRundowns();
return useRundownSource(rundownId, projectRundowns.loaded || null);
}

/**
* Loaded-rundown source for views that must follow the active runtime rundown.
*/
export function useLoadedRundownSource(): RundownSource {
const { data: projectRundowns } = useProjectRundowns();
const loadedRundownId = projectRundowns.loaded || null;
return useRundownSource(loadedRundownId, loadedRundownId);
}

function useRundownSource(rundownId: string | null, loadedRundownId: string | null): RundownSource {
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
const runtimeSelectedEventId = useSelectedEventId();
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
const { data: rundown, status } = useRundownById(rundownId);
const flatRundown = useMemo(
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
[effectiveSelectedEventId, rundown],
);

return useMemo(
() => ({
rundownId,
rundown,
flatRundown,
status,
selectedEventId: effectiveSelectedEventId,
}),
[effectiveSelectedEventId, flatRundown, rundown, rundownId, status],
);
}
23 changes: 15 additions & 8 deletions apps/client/src/common/hooks/useEntryAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ export type EventOptions = Partial<{
}>;

/**
* Gather utilities for actions on entries
* Gather utilities for actions on entries in the loaded rundown.
*/
export const useEntryActions = () => {
export const useEntryActions = () => useEntryActionsForRundown(undefined);

/**
* Gather utilities for actions on entries in an explicitly selected rundown.
*/
export const useScopedEntryActions = (rundownId: string | null) => useEntryActionsForRundown(rundownId ?? '');

function useEntryActionsForRundown(scopedRundownId: string | undefined) {
Comment on lines +73 to +75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve fallback semantics for missing scoped rundown ID.

useScopedEntryActions currently converts null to '', which forces a scoped query key for an empty ID instead of using the loaded-rundown fallback path in useEntryActionsForRundown.

Suggested fix
-export const useScopedEntryActions = (rundownId: string | null) => useEntryActionsForRundown(rundownId ?? '');
+export const useScopedEntryActions = (rundownId: string | null) =>
+  useEntryActionsForRundown(rundownId ?? undefined);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useScopedEntryActions = (rundownId: string | null) => useEntryActionsForRundown(rundownId ?? '');
function useEntryActionsForRundown(scopedRundownId: string | undefined) {
export const useScopedEntryActions = (rundownId: string | null) =>
useEntryActionsForRundown(rundownId ?? undefined);
function useEntryActionsForRundown(scopedRundownId: string | undefined) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/client/src/common/hooks/useEntryAction.ts` around lines 73 - 75, The
helper useScopedEntryActions currently maps a null rundownId to an empty string
which creates an explicit scoped key and bypasses the fallback path in
useEntryActionsForRundown; update useScopedEntryActions so it forwards undefined
(e.g. pass rundownId ?? undefined or conditional if null) instead of '' and keep
the parameter types compatible with useEntryActionsForRundown
(useScopedEntryActions(rundownId: string | null) =>
useEntryActionsForRundown(rundownId ?? undefined)) so the loaded-rundown
fallback path is preserved.

const queryClient = useQueryClient();
const {
linkPrevious,
Expand All @@ -78,12 +85,12 @@ export const useEntryActions = () => {
} = useEditorSettings();

const resolveCurrentRundownQueryKey = useCallback(() => {
const loadedRundownId = queryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
if (loadedRundownId) {
return getRundownQueryKey(loadedRundownId);
if (scopedRundownId !== undefined) {
return getRundownQueryKey(scopedRundownId);
}
return CURRENT_RUNDOWN_QUERY_KEY;
}, [queryClient]);
const loadedRundownId = queryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
return loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY;
}, [queryClient, scopedRundownId]);

/**
* Returns the currently loaded rundown
Expand Down Expand Up @@ -1004,7 +1011,7 @@ export const useEntryActions = () => {
updateTimer,
],
);
};
}

/**
* Utility to optimistically delete entries from client cache
Expand Down
9 changes: 7 additions & 2 deletions apps/client/src/common/utils/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
APP_SETTINGS,
CLIENT_LIST,
CSS_OVERRIDE,
CUSTOM_FIELDS,
CURRENT_RUNDOWN_QUERY_KEY,
CUSTOM_FIELDS,
PROJECT_DATA,
REPORT,
RUNDOWN,
Expand Down Expand Up @@ -213,6 +213,9 @@ export const connectSocket = () => {
case RefetchKey.Settings:
ontimeQueryClient.invalidateQueries({ queryKey: APP_SETTINGS });
break;
case RefetchKey.ProjectRundowns:
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_RUNDOWNS });
break;
default: {
target satisfies never;
break;
Expand Down Expand Up @@ -242,7 +245,9 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s
// skip if we dont recognise the ID the revision is lower
const queryKey = getRundownQueryKey(rundownId);
const cachedRundown = ontimeQueryClient.getQueryData<{ revision: number }>(queryKey);
if (revision !== null && revision === cachedRundown?.revision) {

if (revision === cachedRundown?.revision) {
// we already have the latest change
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IoPencilOutline,
IoTrash,
} from 'react-icons/io5';
import { TbGhost3 } from 'react-icons/tb';

import { downloadAsExcel } from '../../../../common/api/excel';
import { maybeAxiosError } from '../../../../common/api/utils';
Expand All @@ -19,6 +20,7 @@ import { DropdownMenu } from '../../../../common/components/dropdown-menu/Dropdo
import Tag from '../../../../common/components/tag/Tag';
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
import { cx } from '../../../../common/utils/styleUtils';
import { useDirectLinkToBackgroundEdit } from '../../../../views/cuesheet/useCuesheetRundownSelection';
import * as Panel from '../../panel-utils/PanelUtils';
import RundownRenameForm from './composite/RundownRenameForm';
import { ManageRundownForm } from './ManageRundownForm';
Expand Down Expand Up @@ -97,6 +99,8 @@ export default function ManageRundowns() {
await downloadAsExcel(rundownId, title);
};

const navigateToCuesheet = useDirectLinkToBackgroundEdit();

return (
<>
<Panel.Section>
Expand Down Expand Up @@ -180,6 +184,12 @@ export default function ManageRundowns() {
label: 'Duplicate',
onClick: () => submitRundownDuplicate(id),
},
{
type: 'item',
icon: TbGhost3,
label: 'Edit in cuesheet',
onClick: () => navigateToCuesheet(id),
},
{ type: 'divider' },
{
type: 'destructive',
Expand Down
5 changes: 4 additions & 1 deletion apps/client/src/features/rundown/RundownExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
import { useLoadedRundownSource } from '../../common/hooks-query/useScopedRundown';
import { useEntryActions } from '../../common/hooks/useEntryAction';
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
import { handleLinks } from '../../common/utils/linkUtils';
Expand Down Expand Up @@ -104,6 +105,8 @@ interface RundownRootProps {
}

function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
const source = useLoadedRundownSource();

return (
<div className={style.rundownRoot}>
{isSmallDevice ? (
Expand All @@ -112,7 +115,7 @@ function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: Rund
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
)}
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
{viewMode === RundownViewMode.Table && <EntryEditModal />}
{viewMode === RundownViewMode.Table && <EntryEditModal rundown={source.rundown} />}
<RenumberCuesDialog />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { OntimeEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
import { OntimeEntry, Rundown, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
import { useMemo } from 'react';

import useRundown from '../../../common/hooks-query/useRundown';
import EventEditor from './EventEditor';
import GroupEditor from './GroupEditor';
import MilestoneEditor from './MilestoneEditor';
Expand All @@ -10,19 +9,18 @@ import style from './EntryEditor.module.scss';

interface CuesheetEntryEditorProps {
entryId: string;
rundown: Rundown;
}

export default function CuesheetEntryEditor({ entryId }: CuesheetEntryEditorProps) {
const { data } = useRundown();

export default function CuesheetEntryEditor({ entryId, rundown }: CuesheetEntryEditorProps) {
const entry = useMemo<OntimeEntry | null>(() => {
if (data.order.length === 0) {
if (rundown.order.length === 0) {
return null;
}

const event = data.entries[entryId];
const event = rundown.entries[entryId];
return event ?? null;
}, [entryId, data.order, data.entries]);
}, [entryId, rundown.entries, rundown.order.length]);

if (isOntimeEvent(entry)) {
return (
Expand Down

This file was deleted.

Loading
Loading