mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-09 08:19:15 +00:00
refactor: optimistic add elements in UI
This commit is contained in:
committed by
Carlos Valente
parent
4827117163
commit
63cfc9ec19
@@ -6,15 +6,31 @@ import {
|
|||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isOntimeGroup,
|
isOntimeGroup,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
|
OntimeDelay,
|
||||||
OntimeEntry,
|
OntimeEntry,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
|
OntimeGroup,
|
||||||
|
OntimeMilestone,
|
||||||
|
PatchWithId,
|
||||||
Rundown,
|
Rundown,
|
||||||
SupportedEntry,
|
SupportedEntry,
|
||||||
TimeField,
|
TimeField,
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
TransientEventPayload,
|
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils';
|
import {
|
||||||
|
addToRundown,
|
||||||
|
createDelay,
|
||||||
|
createEvent,
|
||||||
|
createGroup,
|
||||||
|
createMilestone,
|
||||||
|
dayInMs,
|
||||||
|
generateId,
|
||||||
|
getInsertAfterId,
|
||||||
|
MILLIS_PER_SECOND,
|
||||||
|
parseUserTime,
|
||||||
|
resolveInsertParent,
|
||||||
|
swapEventData,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
|
import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils';
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
@@ -85,9 +101,64 @@ export const useEntryActions = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: addEntryMutation } = useMutation({
|
const { mutateAsync: addEntryMutation } = useMutation({
|
||||||
mutationFn: ([rundownId, entry]: Parameters<typeof postAddEntry>) => postAddEntry(rundownId, entry),
|
mutationFn: ([rundownId, entry]: [string, PatchWithId & InsertOptions]) => postAddEntry(rundownId, entry),
|
||||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
onMutate: async ([_rundownId, entry]) => {
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
|
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
|
if (previousData) {
|
||||||
|
const optimisticEntry = createOptimisticEntry(entry);
|
||||||
|
const parentId = resolveInsertParent(previousData, entry);
|
||||||
|
const maybeParent = parentId ? previousData.entries[parentId] : null;
|
||||||
|
const parent = maybeParent && isOntimeGroup(maybeParent) ? maybeParent : null;
|
||||||
|
const afterId = getInsertAfterId(previousData, parent, entry.after, entry.before);
|
||||||
|
|
||||||
|
// create a mutable copy — addToRundown mutates in place using immutable array operations
|
||||||
|
const newRundown: Rundown = {
|
||||||
|
...previousData,
|
||||||
|
entries: { ...previousData.entries },
|
||||||
|
revision: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// copy the parent group so we don't mutate the original
|
||||||
|
if (parent && parentId) {
|
||||||
|
newRundown.entries[parentId] = { ...parent, entries: [...parent.entries] };
|
||||||
|
}
|
||||||
|
|
||||||
|
addToRundown(
|
||||||
|
newRundown,
|
||||||
|
optimisticEntry,
|
||||||
|
afterId,
|
||||||
|
parent ? (newRundown.entries[parent.id] as OntimeGroup) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, newRundown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { previousData };
|
||||||
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
if (!response.data) return;
|
||||||
|
|
||||||
|
const serverEntry = response.data;
|
||||||
|
const currentData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
|
||||||
|
if (currentData) {
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
...currentData,
|
||||||
|
entries: { ...currentData.entries, [serverEntry.id]: serverEntry },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (_error, _variables, context) => {
|
||||||
|
if (context?.previousData) {
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, context.previousData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +173,15 @@ export const useEntryActions = () => {
|
|||||||
throw new Error('Rundown not initialised');
|
throw new Error('Rundown not initialised');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry: TransientEventPayload = { ...entry, id: generateId() };
|
const newEntry: PatchWithId & InsertOptions = { ...entry, id: generateId() };
|
||||||
|
|
||||||
|
// handle adding options that concern all event types
|
||||||
|
if (options?.after) {
|
||||||
|
newEntry.after = options.after;
|
||||||
|
}
|
||||||
|
if (options?.before) {
|
||||||
|
newEntry.before = options.before;
|
||||||
|
}
|
||||||
|
|
||||||
// ************* CHECK OPTIONS specific to events
|
// ************* CHECK OPTIONS specific to events
|
||||||
if (isOntimeEvent(newEntry)) {
|
if (isOntimeEvent(newEntry)) {
|
||||||
@@ -142,14 +221,6 @@ export const useEntryActions = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle adding options that concern all event type
|
|
||||||
if (options?.after) {
|
|
||||||
(newEntry as TransientEventPayload).after = options.after;
|
|
||||||
}
|
|
||||||
if (options?.before) {
|
|
||||||
(newEntry as TransientEventPayload).before = options.before;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addEntryMutation([rundownId, newEntry]);
|
await addEntryMutation([rundownId, newEntry]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -905,3 +976,29 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
|
|||||||
|
|
||||||
return { entries, order, flatOrder };
|
return { entries, order, flatOrder };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to create an optimistic entry for immediate cache insertion
|
||||||
|
*/
|
||||||
|
function createOptimisticEntry(payload: PatchWithId & InsertOptions): OntimeEntry {
|
||||||
|
const { after: _after, before: _before, ...entryData } = payload;
|
||||||
|
const id = entryData.id;
|
||||||
|
let parent: EntryId | null = null;
|
||||||
|
if ('parent' in entryData && entryData.parent) {
|
||||||
|
parent = entryData.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entryData.type) {
|
||||||
|
case SupportedEntry.Event: {
|
||||||
|
return createEvent({ ...entryData, id, parent }) as OntimeEvent;
|
||||||
|
}
|
||||||
|
case SupportedEntry.Delay:
|
||||||
|
return createDelay({ id, duration: (entryData as Partial<OntimeDelay>).duration ?? 0, parent });
|
||||||
|
case SupportedEntry.Group:
|
||||||
|
return createGroup({ ...(entryData as Partial<OntimeGroup>), id });
|
||||||
|
case SupportedEntry.Milestone:
|
||||||
|
return createMilestone({ ...(entryData as Partial<OntimeMilestone>), id, parent });
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown entry type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ test('CRUD operations on the rundown', async ({ page }) => {
|
|||||||
// test quick add options - star2+5-t is last end
|
// test quick add options - star2+5-t is last end
|
||||||
await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('20m');
|
await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('20m');
|
||||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:30:00');
|
await expect(page.getByTestId('entry-3').getByTestId('time-input-timeStart')).toHaveValue('00:30:00');
|
||||||
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
|
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
|
||||||
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
|
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
|
||||||
await expect(page.getByTestId('rundown-group')).toHaveCount(1);
|
await expect(page.getByTestId('rundown-group')).toHaveCount(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user