mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
refactor: cache is rundown aware
This commit is contained in:
@@ -11,7 +11,8 @@ export const CUSTOM_VIEWS = ['customViews'];
|
||||
export const PROJECT_DATA = ['project'];
|
||||
export const PROJECT_LIST = ['projectList'];
|
||||
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 URL_PRESETS = ['urlpresets'];
|
||||
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 { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
|
||||
import { fetchCurrentRundown } from '../api/rundown';
|
||||
import { useSelectedEventId } from '../hooks/useSocket';
|
||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
import { useProjectRundowns } from './useProjectRundowns';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
const cachedRundownPlaceholder: Rundown = {
|
||||
@@ -22,12 +23,28 @@ const cachedRundownPlaceholder: Rundown = {
|
||||
* Normalised rundown data
|
||||
*/
|
||||
export default function useRundown() {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
data: { loaded: loadedRundownId },
|
||||
} = useProjectRundowns();
|
||||
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
queryKey: RUNDOWN,
|
||||
queryKey: loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY,
|
||||
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
OntimeGroup,
|
||||
OntimeMilestone,
|
||||
PatchWithId,
|
||||
ProjectRundownsList,
|
||||
Rundown,
|
||||
SupportedEntry,
|
||||
TimeField,
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
ReorderEntry,
|
||||
deleteEntries,
|
||||
@@ -75,12 +76,20 @@ export const useEntryActions = () => {
|
||||
defaultEndAction,
|
||||
} = 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
|
||||
*/
|
||||
const getCurrentRundownData = useCallback(() => {
|
||||
return queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
}, [queryClient]);
|
||||
return queryClient.getQueryData<Rundown>(resolveCurrentRundownQueryKey());
|
||||
}, [queryClient, resolveCurrentRundownQueryKey]);
|
||||
|
||||
/**
|
||||
* Looks for an entry with a given ID in the currently loaded rundown
|
||||
@@ -103,9 +112,10 @@ export const useEntryActions = () => {
|
||||
const { mutateAsync: addEntryMutation } = useMutation({
|
||||
mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(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) {
|
||||
const optimisticEntry = createOptimisticEntry(entry);
|
||||
@@ -133,31 +143,31 @@ export const useEntryActions = () => {
|
||||
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) => {
|
||||
if (!response.data) return;
|
||||
onSuccess: (response, _variables, context) => {
|
||||
if (!response.data || !context?.queryKey) return;
|
||||
|
||||
const serverEntry = response.data;
|
||||
const currentData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const currentData = queryClient.getQueryData<Rundown>(context.queryKey);
|
||||
|
||||
if (currentData) {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, {
|
||||
...currentData,
|
||||
entries: { ...currentData.entries, [serverEntry.id]: serverEntry },
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (_error, _variables, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context.previousData);
|
||||
if (context?.previousData && context?.queryKey) {
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, context.previousData);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -247,8 +257,8 @@ export const useEntryActions = () => {
|
||||
const { mutateAsync: cloneEntryMutation } = useMutation({
|
||||
mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) =>
|
||||
postCloneEntry(rundownId, entryId, options),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -278,11 +288,12 @@ export const useEntryActions = () => {
|
||||
mutationFn: ([rundownId, newEvent]: Parameters<typeof putEditEntry>) => putEditEntry(rundownId, newEvent),
|
||||
// we optimistically update here
|
||||
onMutate: async ([_rundownId, newEvent]) => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(queryKey);
|
||||
const eventId = newEvent.id;
|
||||
|
||||
if (previousData && eventId) {
|
||||
@@ -290,7 +301,7 @@ export const useEntryActions = () => {
|
||||
const newRundown = { ...previousData.entries };
|
||||
// @ts-expect-error -- we expect the events to be of same type
|
||||
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(queryKey, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
@@ -301,18 +312,18 @@ export const useEntryActions = () => {
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousData, newEvent };
|
||||
return { previousData, newEvent, queryKey };
|
||||
},
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (_error, _newEvent, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
if (context?.previousData && context?.queryKey) {
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, context.previousData);
|
||||
}
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
onSettled: async (_data, _error, _variables, context) => {
|
||||
await queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -409,7 +420,7 @@ export const useEntryActions = () => {
|
||||
* Utility function to get the previous event end time
|
||||
*/
|
||||
function getPreviousEnd(): number {
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const cachedRundown = queryClient.getQueryData<Rundown>(resolveCurrentRundownQueryKey());
|
||||
|
||||
if (!cachedRundown?.order || !cachedRundown?.entries) {
|
||||
return 0;
|
||||
@@ -440,11 +451,12 @@ export const useEntryActions = () => {
|
||||
const { mutateAsync: batchUpdateEventsMutation } = useMutation({
|
||||
mutationFn: ([rundownId, data]: Parameters<typeof putBatchEditEvents>) => putBatchEditEvents(rundownId, data),
|
||||
onMutate: async ([_rundownId, data]) => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousRundown = queryClient.getQueryData<Rundown>(queryKey);
|
||||
|
||||
if (previousRundown) {
|
||||
const eventIds = new Set(data.ids);
|
||||
@@ -462,7 +474,7 @@ export const useEntryActions = () => {
|
||||
}
|
||||
});
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(queryKey, {
|
||||
id: previousRundown.id,
|
||||
title: previousRundown.title,
|
||||
order: previousRundown.order,
|
||||
@@ -473,13 +485,13 @@ export const useEntryActions = () => {
|
||||
}
|
||||
|
||||
// Return a context with the previous rundown
|
||||
return { previousRundown };
|
||||
return { previousRundown, queryKey };
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) return;
|
||||
onSuccess: (response, _variables, context) => {
|
||||
if (!response.data || !context?.queryKey) return;
|
||||
|
||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, {
|
||||
id,
|
||||
title,
|
||||
order,
|
||||
@@ -489,7 +501,9 @@ export const useEntryActions = () => {
|
||||
});
|
||||
},
|
||||
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),
|
||||
// we optimistically update here
|
||||
onMutate: async ([_rundownId, entryIds]) => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(queryKey);
|
||||
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const { entries, order, flatOrder } = optimisticDeleteEntries(entryIds, previousData);
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(queryKey, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order,
|
||||
@@ -538,17 +553,19 @@ export const useEntryActions = () => {
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousData };
|
||||
return { previousData, queryKey };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
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
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -579,14 +596,15 @@ export const useEntryActions = () => {
|
||||
mutationFn: ([rundownId]: Parameters<typeof requestDeleteAll>) => requestDeleteAll(rundownId),
|
||||
// we optimistically update here
|
||||
onMutate: async () => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(queryKey);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(queryKey, {
|
||||
id: previousData?.id ?? 'default',
|
||||
title: previousData?.title ?? '',
|
||||
order: [],
|
||||
@@ -596,17 +614,19 @@ export const useEntryActions = () => {
|
||||
});
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousData };
|
||||
return { previousData, queryKey };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback optimist update
|
||||
onError: (_error, _, context) => {
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
||||
if (context?.queryKey) {
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, context?.previousData);
|
||||
}
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -632,12 +652,12 @@ export const useEntryActions = () => {
|
||||
*/
|
||||
const { mutateAsync: applyDelayMutation } = useMutation({
|
||||
mutationFn: ([rundownId, delayId]: Parameters<typeof requestApplyDelay>) => requestApplyDelay(rundownId, delayId),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) return;
|
||||
|
||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
|
||||
id,
|
||||
title,
|
||||
order,
|
||||
@@ -648,7 +668,7 @@ export const useEntryActions = () => {
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -677,12 +697,12 @@ export const useEntryActions = () => {
|
||||
*/
|
||||
const { mutateAsync: ungroupMutation } = useMutation({
|
||||
mutationFn: ([rundownId, groupId]: Parameters<typeof requestUngroup>) => requestUngroup(rundownId, groupId),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) return;
|
||||
|
||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
|
||||
id,
|
||||
title,
|
||||
order,
|
||||
@@ -691,7 +711,7 @@ export const useEntryActions = () => {
|
||||
revision,
|
||||
});
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -720,12 +740,12 @@ export const useEntryActions = () => {
|
||||
const { mutateAsync: groupEntriesMutation } = useMutation({
|
||||
mutationFn: ([rundownId, entryIds]: Parameters<typeof requestGroupEntries>) =>
|
||||
requestGroupEntries(rundownId, entryIds),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) return;
|
||||
|
||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(resolveCurrentRundownQueryKey(), {
|
||||
id,
|
||||
title,
|
||||
order,
|
||||
@@ -734,7 +754,7 @@ export const useEntryActions = () => {
|
||||
revision,
|
||||
});
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -771,9 +791,9 @@ export const useEntryActions = () => {
|
||||
*/
|
||||
const { mutateAsync: reorderEntryMutation } = useMutation({
|
||||
mutationFn: ([rundownId, data]: Parameters<typeof patchReorderEntry>) => patchReorderEntry(rundownId, data),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: resolveCurrentRundownQueryKey() }),
|
||||
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),
|
||||
// we optimistically update here
|
||||
onMutate: async ([_rundownId, from, to]) => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const previousData = queryClient.getQueryData<Rundown>(queryKey);
|
||||
if (previousData) {
|
||||
// optimistically update object
|
||||
const newRundown = { ...previousData.entries };
|
||||
@@ -867,7 +888,7 @@ export const useEntryActions = () => {
|
||||
newRundown[from] = newA;
|
||||
newRundown[to] = newB;
|
||||
|
||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||
queryClient.setQueryData<Rundown>(queryKey, {
|
||||
id: previousData.id,
|
||||
title: previousData.title,
|
||||
order: previousData.order,
|
||||
@@ -878,17 +899,19 @@ export const useEntryActions = () => {
|
||||
}
|
||||
|
||||
// Return a context with the previous events
|
||||
return { previousData };
|
||||
return { previousData, queryKey };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
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
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: context?.queryKey ?? resolveCurrentRundownQueryKey() });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
ApiActionTag,
|
||||
Log,
|
||||
MaybeNumber,
|
||||
MessageTag,
|
||||
ProjectRundownsList,
|
||||
RefetchKey,
|
||||
Rundown,
|
||||
RuntimeStore,
|
||||
@@ -16,12 +18,14 @@ import {
|
||||
CSS_OVERRIDE,
|
||||
CUSTOM_FIELDS,
|
||||
PROJECT_DATA,
|
||||
CURRENT_RUNDOWN_QUERY_KEY,
|
||||
PROJECT_RUNDOWNS,
|
||||
REPORT,
|
||||
RUNDOWN,
|
||||
RUNTIME,
|
||||
TRANSLATION,
|
||||
URL_PRESETS,
|
||||
VIEW_SETTINGS,
|
||||
getRundownQueryKey,
|
||||
} from '../api/constants';
|
||||
import { invalidateAllCaches } from '../api/utils';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
@@ -189,11 +193,10 @@ export const connectSocket = () => {
|
||||
case RefetchKey.Report:
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||
break;
|
||||
case RefetchKey.Rundown:
|
||||
if (revision === (ontimeQueryClient.getQueryData(RUNDOWN) as Rundown).revision) break;
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||
case RefetchKey.Rundown: {
|
||||
maybeInvalidateRundownCache(revision);
|
||||
break;
|
||||
}
|
||||
case RefetchKey.UrlPresets:
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: URL_PRESETS });
|
||||
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>(
|
||||
tag: T,
|
||||
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
|
||||
|
||||
+11
-5
@@ -1,8 +1,13 @@
|
||||
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 { 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 {
|
||||
previewRundown,
|
||||
@@ -81,9 +86,10 @@ export default function useGoogleSheet() {
|
||||
await patchData({ rundowns, customFields });
|
||||
// we are unable to optimistically set the rundown since we need
|
||||
// it to be normalised
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [RUNDOWN, CUSTOM_FIELDS],
|
||||
});
|
||||
const loadedRundownId = queryClient.getQueryData<ProjectRundownsList>(PROJECT_RUNDOWNS)?.loaded;
|
||||
const rundownQueryKey = loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY;
|
||||
await queryClient.invalidateQueries({ queryKey: rundownQueryKey });
|
||||
await queryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||
} catch (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 { 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 { 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
|
||||
if (selectMode === 'ctrl') {
|
||||
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const rundownData = getLoadedRundownData();
|
||||
if (!rundownData) return;
|
||||
|
||||
// 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
|
||||
if (selectMode === 'shift') {
|
||||
const rundownData = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const rundownData = getLoadedRundownData();
|
||||
if (!rundownData) return;
|
||||
|
||||
// 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 {
|
||||
if ((isMacOS() && event.metaKey) || event.ctrlKey) {
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user