refactor: cache is rundown aware

This commit is contained in:
Carlos Valente
2026-04-04 14:58:58 +02:00
committed by GitHub
parent 3022ec2acd
commit 8c14c330f5
7 changed files with 253 additions and 85 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ export const CUSTOM_VIEWS = ['customViews'];
export const PROJECT_DATA = ['project']; export const PROJECT_DATA = ['project'];
export const PROJECT_LIST = ['projectList']; export const PROJECT_LIST = ['projectList'];
export const PROJECT_RUNDOWNS = ['projectRundowns']; export const PROJECT_RUNDOWNS = ['projectRundowns'];
export const RUNDOWN = ['rundown']; export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
export const RUNTIME = ['runtimeStore']; export const RUNTIME = ['runtimeStore'];
export const URL_PRESETS = ['urlpresets']; export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings']; export const VIEW_SETTINGS = ['viewSettings'];
@@ -1,12 +1,13 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { EntryId, OntimeEntry, Rundown } from 'ontime-types'; import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
import { useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { RUNDOWN } from '../api/constants'; import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
import { fetchCurrentRundown } from '../api/rundown'; import { fetchCurrentRundown } from '../api/rundown';
import { useSelectedEventId } from '../hooks/useSocket'; import { useSelectedEventId } from '../hooks/useSocket';
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata'; import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
import { useProjectRundowns } from './useProjectRundowns';
// revision is -1 so that the remote revision is higher // revision is -1 so that the remote revision is higher
const cachedRundownPlaceholder: Rundown = { const cachedRundownPlaceholder: Rundown = {
@@ -22,12 +23,28 @@ const cachedRundownPlaceholder: Rundown = {
* Normalised rundown data * Normalised rundown data
*/ */
export default function useRundown() { export default function useRundown() {
const queryClient = useQueryClient();
const {
data: { loaded: loadedRundownId },
} = useProjectRundowns();
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({ const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
queryKey: RUNDOWN, queryKey: loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY,
queryFn: ({ signal }) => fetchCurrentRundown({ signal }), queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
// Seed the ID-based cache when fetching via the 'current' alias (bootstrap)
useEffect(() => {
if (!data || loadedRundownId) return;
queryClient.setQueryData(getRundownQueryKey(data.id), data);
}, [data, loadedRundownId, queryClient]);
useEffect(() => {
if (!loadedRundownId) return;
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
}, [loadedRundownId, queryClient]);
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching }; return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
} }
+89 -66
View File
@@ -9,6 +9,7 @@ import {
OntimeGroup, OntimeGroup,
OntimeMilestone, OntimeMilestone,
PatchWithId, PatchWithId,
ProjectRundownsList,
Rundown, Rundown,
SupportedEntry, SupportedEntry,
TimeField, TimeField,
@@ -33,7 +34,7 @@ import {
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils'; import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
import { RUNDOWN } from '../api/constants'; import { CURRENT_RUNDOWN_QUERY_KEY, PROJECT_RUNDOWNS, getRundownQueryKey } from '../api/constants';
import { import {
ReorderEntry, ReorderEntry,
deleteEntries, deleteEntries,
@@ -75,12 +76,20 @@ export const useEntryActions = () => {
defaultEndAction, defaultEndAction,
} = useEditorSettings(); } = useEditorSettings();
const resolveCurrentRundownQueryKey = useCallback(() => {
const loadedRundownId = queryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
if (loadedRundownId) {
return getRundownQueryKey(loadedRundownId);
}
return CURRENT_RUNDOWN_QUERY_KEY;
}, [queryClient]);
/** /**
* Returns the currently loaded rundown * Returns the currently loaded rundown
*/ */
const getCurrentRundownData = useCallback(() => { const getCurrentRundownData = useCallback(() => {
return queryClient.getQueryData<Rundown>(RUNDOWN); return queryClient.getQueryData<Rundown>(resolveCurrentRundownQueryKey());
}, [queryClient]); }, [queryClient, resolveCurrentRundownQueryKey]);
/** /**
* Looks for an entry with a given ID in the currently loaded rundown * Looks for an entry with a given ID in the currently loaded rundown
@@ -103,9 +112,10 @@ export const useEntryActions = () => {
const { mutateAsync: addEntryMutation } = useMutation({ const { mutateAsync: addEntryMutation } = useMutation({
mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry), mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry),
onMutate: async ([_rundownId, entry]) => { onMutate: async ([_rundownId, entry]) => {
await queryClient.cancelQueries({ queryKey: RUNDOWN }); const queryKey = resolveCurrentRundownQueryKey();
await queryClient.cancelQueries({ queryKey });
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN); const previousData = queryClient.getQueryData<Rundown>(queryKey);
if (previousData) { if (previousData) {
const optimisticEntry = createOptimisticEntry(entry); const optimisticEntry = createOptimisticEntry(entry);
@@ -133,31 +143,31 @@ export const useEntryActions = () => {
parent ? (newRundown.entries[parent.id] as OntimeGroup) : null, parent ? (newRundown.entries[parent.id] as OntimeGroup) : null,
); );
queryClient.setQueryData<Rundown>(RUNDOWN, newRundown); queryClient.setQueryData<Rundown>(queryKey, newRundown);
} }
return { previousData }; return { previousData, queryKey };
}, },
onSuccess: (response) => { onSuccess: (response, _variables, context) => {
if (!response.data) return; if (!response.data || !context?.queryKey) return;
const serverEntry = response.data; const serverEntry = response.data;
const currentData = queryClient.getQueryData<Rundown>(RUNDOWN); const currentData = queryClient.getQueryData<Rundown>(context.queryKey);
if (currentData) { if (currentData) {
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(context.queryKey, {
...currentData, ...currentData,
entries: { ...currentData.entries, [serverEntry.id]: serverEntry }, entries: { ...currentData.entries, [serverEntry.id]: serverEntry },
}); });
} }
}, },
onError: (_error, _variables, context) => { onError: (_error, _variables, context) => {
if (context?.previousData) { if (context?.previousData && context?.queryKey) {
queryClient.setQueryData<Rundown>(RUNDOWN, context.previousData); queryClient.setQueryData<Rundown>(context.queryKey, context.previousData);
} }
}, },
onSettled: () => { onSettled: (_data, _error, _variables, context) => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -247,8 +257,8 @@ export const useEntryActions = () => {
const { mutateAsync: cloneEntryMutation } = useMutation({ const { mutateAsync: cloneEntryMutation } = useMutation({
mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) => mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) =>
postCloneEntry(rundownId, entryId, options), postCloneEntry(rundownId, entryId, options),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }), onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
}); });
/** /**
@@ -278,11 +288,12 @@ export const useEntryActions = () => {
mutationFn: ([rundownId, newEvent]: Parameters<typeof putEditEntry>) => putEditEntry(rundownId, newEvent), mutationFn: ([rundownId, newEvent]: Parameters<typeof putEditEntry>) => putEditEntry(rundownId, newEvent),
// we optimistically update here // we optimistically update here
onMutate: async ([_rundownId, newEvent]) => { onMutate: async ([_rundownId, newEvent]) => {
const queryKey = resolveCurrentRundownQueryKey();
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey });
// Snapshot the previous value // Snapshot the previous value
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN); const previousData = queryClient.getQueryData<Rundown>(queryKey);
const eventId = newEvent.id; const eventId = newEvent.id;
if (previousData && eventId) { if (previousData && eventId) {
@@ -290,7 +301,7 @@ export const useEntryActions = () => {
const newRundown = { ...previousData.entries }; const newRundown = { ...previousData.entries };
// @ts-expect-error -- we expect the events to be of same type // @ts-expect-error -- we expect the events to be of same type
newRundown[eventId] = { ...newRundown[eventId], ...newEvent }; newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(queryKey, {
id: previousData.id, id: previousData.id,
title: previousData.title, title: previousData.title,
order: previousData.order, order: previousData.order,
@@ -301,18 +312,18 @@ export const useEntryActions = () => {
} }
// Return a context with the previous and new events // Return a context with the previous and new events
return { previousData, newEvent }; return { previousData, newEvent, queryKey };
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (_error, _newEvent, context) => { onError: (_error, _newEvent, context) => {
if (context?.previousData) { if (context?.previousData && context?.queryKey) {
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData); queryClient.setQueryData<Rundown>(context.queryKey, context.previousData);
} }
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: async () => { onSettled: async (_data, _error, _variables, context) => {
await queryClient.invalidateQueries({ queryKey: RUNDOWN }); await queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -409,7 +420,7 @@ export const useEntryActions = () => {
* Utility function to get the previous event end time * Utility function to get the previous event end time
*/ */
function getPreviousEnd(): number { function getPreviousEnd(): number {
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN); const cachedRundown = queryClient.getQueryData<Rundown>(resolveCurrentRundownQueryKey());
if (!cachedRundown?.order || !cachedRundown?.entries) { if (!cachedRundown?.order || !cachedRundown?.entries) {
return 0; return 0;
@@ -440,11 +451,12 @@ export const useEntryActions = () => {
const { mutateAsync: batchUpdateEventsMutation } = useMutation({ const { mutateAsync: batchUpdateEventsMutation } = useMutation({
mutationFn: ([rundownId, data]: Parameters<typeof putBatchEditEvents>) => putBatchEditEvents(rundownId, data), mutationFn: ([rundownId, data]: Parameters<typeof putBatchEditEvents>) => putBatchEditEvents(rundownId, data),
onMutate: async ([_rundownId, data]) => { onMutate: async ([_rundownId, data]) => {
const queryKey = resolveCurrentRundownQueryKey();
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey });
// Snapshot the previous value // Snapshot the previous value
const previousRundown = queryClient.getQueryData<Rundown>(RUNDOWN); const previousRundown = queryClient.getQueryData<Rundown>(queryKey);
if (previousRundown) { if (previousRundown) {
const eventIds = new Set(data.ids); const eventIds = new Set(data.ids);
@@ -462,7 +474,7 @@ export const useEntryActions = () => {
} }
}); });
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(queryKey, {
id: previousRundown.id, id: previousRundown.id,
title: previousRundown.title, title: previousRundown.title,
order: previousRundown.order, order: previousRundown.order,
@@ -473,13 +485,13 @@ export const useEntryActions = () => {
} }
// Return a context with the previous rundown // Return a context with the previous rundown
return { previousRundown }; return { previousRundown, queryKey };
}, },
onSuccess: (response) => { onSuccess: (response, _variables, context) => {
if (!response.data) return; if (!response.data || !context?.queryKey) return;
const { id, title, order, flatOrder, entries, revision } = response.data; const { id, title, order, flatOrder, entries, revision } = response.data;
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(context.queryKey, {
id, id,
title, title,
order, order,
@@ -489,7 +501,9 @@ export const useEntryActions = () => {
}); });
}, },
onError: (_error, _newEvent, context) => { onError: (_error, _newEvent, context) => {
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousRundown); if (context?.queryKey) {
queryClient.setQueryData<Rundown>(context.queryKey, context?.previousRundown);
}
}, },
}); });
@@ -517,17 +531,18 @@ export const useEntryActions = () => {
mutationFn: ([rundownId, entryIds]: Parameters<typeof deleteEntries>) => deleteEntries(rundownId, entryIds), mutationFn: ([rundownId, entryIds]: Parameters<typeof deleteEntries>) => deleteEntries(rundownId, entryIds),
// we optimistically update here // we optimistically update here
onMutate: async ([_rundownId, entryIds]) => { onMutate: async ([_rundownId, entryIds]) => {
const queryKey = resolveCurrentRundownQueryKey();
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey });
// Snapshot the previous value // Snapshot the previous value
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN); const previousData = queryClient.getQueryData<Rundown>(queryKey);
if (previousData) { if (previousData) {
// optimistically update object // optimistically update object
const { entries, order, flatOrder } = optimisticDeleteEntries(entryIds, previousData); const { entries, order, flatOrder } = optimisticDeleteEntries(entryIds, previousData);
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(queryKey, {
id: previousData.id, id: previousData.id,
title: previousData.title, title: previousData.title,
order, order,
@@ -538,17 +553,19 @@ export const useEntryActions = () => {
} }
// Return a context with the previous and new events // Return a context with the previous and new events
return { previousData }; return { previousData, queryKey };
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (_error, _entryIds, context) => { onError: (_error, _entryIds, context) => {
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData); if (context?.queryKey) {
queryClient.setQueryData<Rundown>(context.queryKey, context?.previousData);
}
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: (_data, _error, _variables, context) => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -579,14 +596,15 @@ export const useEntryActions = () => {
mutationFn: ([rundownId]: Parameters<typeof requestDeleteAll>) => requestDeleteAll(rundownId), mutationFn: ([rundownId]: Parameters<typeof requestDeleteAll>) => requestDeleteAll(rundownId),
// we optimistically update here // we optimistically update here
onMutate: async () => { onMutate: async () => {
const queryKey = resolveCurrentRundownQueryKey();
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey });
// Snapshot the previous value // Snapshot the previous value
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN); const previousData = queryClient.getQueryData<Rundown>(queryKey);
// optimistically update object // optimistically update object
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(queryKey, {
id: previousData?.id ?? 'default', id: previousData?.id ?? 'default',
title: previousData?.title ?? '', title: previousData?.title ?? '',
order: [], order: [],
@@ -596,17 +614,19 @@ export const useEntryActions = () => {
}); });
// Return a context with the previous and new events // Return a context with the previous and new events
return { previousData }; return { previousData, queryKey };
}, },
// Mutation fails, rollback optimist update // Mutation fails, rollback optimist update
onError: (_error, _, context) => { onError: (_error, _, context) => {
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData); if (context?.queryKey) {
queryClient.setQueryData<Rundown>(context.queryKey, context?.previousData);
}
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: (_data, _error, _variables, context) => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -632,12 +652,12 @@ export const useEntryActions = () => {
*/ */
const { mutateAsync: applyDelayMutation } = useMutation({ const { mutateAsync: applyDelayMutation } = useMutation({
mutationFn: ([rundownId, delayId]: Parameters<typeof requestApplyDelay>) => requestApplyDelay(rundownId, delayId), mutationFn: ([rundownId, delayId]: Parameters<typeof requestApplyDelay>) => requestApplyDelay(rundownId, delayId),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
onSuccess: (response) => { onSuccess: (response) => {
if (!response.data) return; if (!response.data) return;
const { id, title, order, flatOrder, entries, revision } = response.data; const { id, title, order, flatOrder, entries, revision } = response.data;
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
id, id,
title, title,
order, order,
@@ -648,7 +668,7 @@ export const useEntryActions = () => {
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -677,12 +697,12 @@ export const useEntryActions = () => {
*/ */
const { mutateAsync: ungroupMutation } = useMutation({ const { mutateAsync: ungroupMutation } = useMutation({
mutationFn: ([rundownId, groupId]: Parameters<typeof requestUngroup>) => requestUngroup(rundownId, groupId), mutationFn: ([rundownId, groupId]: Parameters<typeof requestUngroup>) => requestUngroup(rundownId, groupId),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
onSuccess: (response) => { onSuccess: (response) => {
if (!response.data) return; if (!response.data) return;
const { id, title, order, flatOrder, entries, revision } = response.data; const { id, title, order, flatOrder, entries, revision } = response.data;
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
id, id,
title, title,
order, order,
@@ -691,7 +711,7 @@ export const useEntryActions = () => {
revision, revision,
}); });
}, },
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }), onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
}); });
/** /**
@@ -720,12 +740,12 @@ export const useEntryActions = () => {
const { mutateAsync: groupEntriesMutation } = useMutation({ const { mutateAsync: groupEntriesMutation } = useMutation({
mutationFn: ([rundownId, entryIds]: Parameters<typeof requestGroupEntries>) => mutationFn: ([rundownId, entryIds]: Parameters<typeof requestGroupEntries>) =>
requestGroupEntries(rundownId, entryIds), requestGroupEntries(rundownId, entryIds),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
onSuccess: (response) => { onSuccess: (response) => {
if (!response.data) return; if (!response.data) return;
const { id, title, order, flatOrder, entries, revision } = response.data; const { id, title, order, flatOrder, entries, revision } = response.data;
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
id, id,
title, title,
order, order,
@@ -734,7 +754,7 @@ export const useEntryActions = () => {
revision, revision,
}); });
}, },
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }), onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
}); });
/** /**
@@ -771,9 +791,9 @@ export const useEntryActions = () => {
*/ */
const { mutateAsync: reorderEntryMutation } = useMutation({ const { mutateAsync: reorderEntryMutation } = useMutation({
mutationFn: ([rundownId, data]: Parameters<typeof patchReorderEntry>) => patchReorderEntry(rundownId, data), mutationFn: ([rundownId, data]: Parameters<typeof patchReorderEntry>) => patchReorderEntry(rundownId, data),
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }), onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() });
}, },
}); });
@@ -848,11 +868,12 @@ export const useEntryActions = () => {
mutationFn: ([rundownId, from, to]: Parameters<typeof requestEventSwap>) => requestEventSwap(rundownId, from, to), mutationFn: ([rundownId, from, to]: Parameters<typeof requestEventSwap>) => requestEventSwap(rundownId, from, to),
// we optimistically update here // we optimistically update here
onMutate: async ([_rundownId, from, to]) => { onMutate: async ([_rundownId, from, to]) => {
const queryKey = resolveCurrentRundownQueryKey();
// cancel ongoing queries // cancel ongoing queries
await queryClient.cancelQueries({ queryKey: RUNDOWN }); await queryClient.cancelQueries({ queryKey });
// Snapshot the previous value // Snapshot the previous value
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN); const previousData = queryClient.getQueryData<Rundown>(queryKey);
if (previousData) { if (previousData) {
// optimistically update object // optimistically update object
const newRundown = { ...previousData.entries }; const newRundown = { ...previousData.entries };
@@ -867,7 +888,7 @@ export const useEntryActions = () => {
newRundown[from] = newA; newRundown[from] = newA;
newRundown[to] = newB; newRundown[to] = newB;
queryClient.setQueryData<Rundown>(RUNDOWN, { queryClient.setQueryData<Rundown>(queryKey, {
id: previousData.id, id: previousData.id,
title: previousData.title, title: previousData.title,
order: previousData.order, order: previousData.order,
@@ -878,17 +899,19 @@ export const useEntryActions = () => {
} }
// Return a context with the previous events // Return a context with the previous events
return { previousData }; return { previousData, queryKey };
}, },
// Mutation fails, rollback undoes optimist update // Mutation fails, rollback undoes optimist update
onError: (_error, _eventId, context) => { onError: (_error, _eventId, context) => {
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData); if (context?.queryKey) {
queryClient.setQueryData<Rundown>(context.queryKey, context?.previousData);
}
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: (_data, _error, _variables, context) => {
queryClient.invalidateQueries({ queryKey: RUNDOWN }); queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
}, },
}); });
+38 -5
View File
@@ -1,7 +1,9 @@
import { import {
ApiActionTag, ApiActionTag,
Log, Log,
MaybeNumber,
MessageTag, MessageTag,
ProjectRundownsList,
RefetchKey, RefetchKey,
Rundown, Rundown,
RuntimeStore, RuntimeStore,
@@ -16,12 +18,14 @@ import {
CSS_OVERRIDE, CSS_OVERRIDE,
CUSTOM_FIELDS, CUSTOM_FIELDS,
PROJECT_DATA, PROJECT_DATA,
CURRENT_RUNDOWN_QUERY_KEY,
PROJECT_RUNDOWNS,
REPORT, REPORT,
RUNDOWN,
RUNTIME, RUNTIME,
TRANSLATION, TRANSLATION,
URL_PRESETS, URL_PRESETS,
VIEW_SETTINGS, VIEW_SETTINGS,
getRundownQueryKey,
} from '../api/constants'; } from '../api/constants';
import { invalidateAllCaches } from '../api/utils'; import { invalidateAllCaches } from '../api/utils';
import { ontimeQueryClient } from '../queryClient'; import { ontimeQueryClient } from '../queryClient';
@@ -189,11 +193,10 @@ export const connectSocket = () => {
case RefetchKey.Report: case RefetchKey.Report:
ontimeQueryClient.invalidateQueries({ queryKey: REPORT }); ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
break; break;
case RefetchKey.Rundown: case RefetchKey.Rundown: {
if (revision === (ontimeQueryClient.getQueryData(RUNDOWN) as Rundown).revision) break; maybeInvalidateRundownCache(revision);
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
break; break;
}
case RefetchKey.UrlPresets: case RefetchKey.UrlPresets:
ontimeQueryClient.invalidateQueries({ queryKey: URL_PRESETS }); ontimeQueryClient.invalidateQueries({ queryKey: URL_PRESETS });
break; break;
@@ -227,6 +230,36 @@ export const connectSocket = () => {
}; };
}; };
/**
* When we receive a refetch message for the rundown
* check which rundown needs to be invalidated
*/
export function maybeInvalidateRundownCache(revision: MaybeNumber) {
const loadedRundownId: string | undefined = (ontimeQueryClient.getQueryData(PROJECT_RUNDOWNS) as ProjectRundownsList)
?.loaded;
const activeRundownQueryKey = loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY;
const cachedRundown = ontimeQueryClient.getQueryData<Rundown>(activeRundownQueryKey);
if (revision === cachedRundown?.revision) {
return;
}
ontimeQueryClient.invalidateQueries({ queryKey: activeRundownQueryKey, exact: true });
if (loadedRundownId) {
// Keep bootstrap alias in sync with the ID-based cache
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
} else {
// During bootstrap, loadedRundownId is not yet known.
// Invalidate any ID-based rundown caches that may have been seeded early.
ontimeQueryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'rundown' && query.queryKey[1] !== 'current',
});
}
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
}
export function sendSocket<T extends MessageTag | ApiActionTag>( export function sendSocket<T extends MessageTag | ApiActionTag>(
tag: T, tag: T,
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown, payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
@@ -1,8 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types'; import { AuthenticationStatus, CustomFields, ProjectRundowns, ProjectRundownsList } from 'ontime-types';
import { ImportMap } from 'ontime-utils'; import { ImportMap } from 'ontime-utils';
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../../common/api/constants'; import {
CURRENT_RUNDOWN_QUERY_KEY,
CUSTOM_FIELDS,
PROJECT_RUNDOWNS,
getRundownQueryKey,
} from '../../../../../common/api/constants';
import { patchData } from '../../../../../common/api/db'; import { patchData } from '../../../../../common/api/db';
import { import {
previewRundown, previewRundown,
@@ -81,9 +86,10 @@ export default function useGoogleSheet() {
await patchData({ rundowns, customFields }); await patchData({ rundowns, customFields });
// we are unable to optimistically set the rundown since we need // we are unable to optimistically set the rundown since we need
// it to be normalised // it to be normalised
await queryClient.invalidateQueries({ const loadedRundownId = queryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
queryKey: [RUNDOWN, CUSTOM_FIELDS], const rundownQueryKey = loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY;
}); await queryClient.invalidateQueries({ queryKey: rundownQueryKey });
await queryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
} catch (error) { } catch (error) {
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } }); patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
} }
@@ -1,8 +1,8 @@
import { EntryId, MaybeNumber, Rundown, isOntimeEvent } from 'ontime-types'; import { EntryId, MaybeNumber, ProjectRundownsList, Rundown, isOntimeEvent } from 'ontime-types';
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { create } from 'zustand'; import { create } from 'zustand';
import { RUNDOWN } from '../../common/api/constants'; import { CURRENT_RUNDOWN_QUERY_KEY, PROJECT_RUNDOWNS, getRundownQueryKey } from '../../common/api/constants';
import { ontimeQueryClient } from '../../common/queryClient'; import { ontimeQueryClient } from '../../common/queryClient';
import { isMacOS } from '../../common/utils/deviceUtils'; import { isMacOS } from '../../common/utils/deviceUtils';
@@ -51,7 +51,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
// on ctrl + click, we toggle the selection of that event // on ctrl + click, we toggle the selection of that event
if (selectMode === 'ctrl') { if (selectMode === 'ctrl') {
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN); const rundownData = getLoadedRundownData();
if (!rundownData) return; if (!rundownData) return;
// if it doesnt exist, simply add to the list and set an anchor // if it doesnt exist, simply add to the list and set an anchor
@@ -82,7 +82,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
// on shift + click, we select a range of events up to the clicked event // on shift + click, we select a range of events up to the clicked event
if (selectMode === 'shift') { if (selectMode === 'shift') {
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN); const rundownData = getLoadedRundownData();
if (!rundownData) return; if (!rundownData) return;
// get list of rundown with only ontime events // get list of rundown with only ontime events
@@ -136,6 +136,15 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
}, },
})); }));
function getLoadedRundownData() {
const loadedRundownId = ontimeQueryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
if (loadedRundownId) {
return ontimeQueryClient.getQueryData<Rundown>(getRundownQueryKey(loadedRundownId));
}
return ontimeQueryClient.getQueryData<Rundown>(CURRENT_RUNDOWN_QUERY_KEY);
}
export function getSelectionMode(event: MouseEvent): SelectionMode { export function getSelectionMode(event: MouseEvent): SelectionMode {
if ((isMacOS() && event.metaKey) || event.ctrlKey) { if ((isMacOS() && event.metaKey) || event.ctrlKey) {
return 'ctrl'; return 'ctrl';
@@ -0,0 +1,79 @@
import { expect, test } from '@playwright/test';
test('switching rundowns preserves edits per rundown', async ({ page }) => {
const suffix = Date.now();
const nameA = `Rundown A ${suffix}`;
const nameB = `Rundown B ${suffix}`;
await page.goto('/editor');
await expect(page.getByTestId('editor-container')).toBeVisible();
const editButton = page.getByRole('button', { name: 'Edit' }).first();
await editButton.click();
// open manage rundowns and create Rundown A
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Manage Rundowns...' }).click();
await page.getByRole('heading', { name: 'Manage project rundowns' }).getByRole('button', { name: 'New' }).click();
await page.getByPlaceholder('Your rundown name').fill(nameA);
await page.getByRole('button', { name: 'Create rundown' }).click();
await expect(page.getByRole('row', { name: nameA })).toBeVisible();
// load Rundown A
await page.getByRole('row', { name: nameA }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Load', exact: true }).click();
await page.getByRole('button', { name: 'Load rundown' }).click();
await expect(page.getByRole('row', { name: nameA }).getByText('Loaded')).toBeVisible();
// close settings and add an event to Rundown A
await page.getByRole('button', { name: 'Close settings' }).click();
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').getByTestId('entry__title').fill('Event in rundown A');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
// open manage rundowns and create Rundown B
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Manage Rundowns...' }).click();
await page.getByRole('heading', { name: 'Manage project rundowns' }).getByRole('button', { name: 'New' }).click();
await page.getByPlaceholder('Your rundown name').fill(nameB);
await page.getByRole('button', { name: 'Create rundown' }).click();
await expect(page.getByRole('row', { name: nameB })).toBeVisible();
// load Rundown B
await page.getByRole('row', { name: nameB }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Load', exact: true }).click();
await page.getByRole('button', { name: 'Load rundown' }).click();
await expect(page.getByRole('row', { name: nameB }).getByText('Loaded')).toBeVisible();
// close settings and add an event to Rundown B
await page.getByRole('button', { name: 'Close settings' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').getByTestId('entry__title').fill('Event in rundown B');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
// switch back to Rundown A and verify its event is preserved
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Manage Rundowns...' }).click();
await page.getByRole('row', { name: nameA }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Load', exact: true }).click();
await page.getByRole('button', { name: 'Load rundown' }).click();
await page.getByRole('button', { name: 'Close settings' }).click();
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('Event in rundown A');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
// switch back to Rundown B and verify its event is preserved
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Manage Rundowns...' }).click();
await page.getByRole('row', { name: nameB }).getByRole('button').click();
await page.getByRole('menuitem', { name: 'Load', exact: true }).click();
await page.getByRole('button', { name: 'Load rundown' }).click();
await page.getByRole('button', { name: 'Close settings' }).click();
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('Event in rundown B');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
});