diff --git a/apps/client/src/common/hooks/useEntryAction.ts b/apps/client/src/common/hooks/useEntryAction.ts index 4600c3618..8ac0c669d 100644 --- a/apps/client/src/common/hooks/useEntryAction.ts +++ b/apps/client/src/common/hooks/useEntryAction.ts @@ -4,6 +4,7 @@ import { EntryId, isOntimeEvent, MaybeString, + OntimeBlock, OntimeEntry, OntimeEvent, Rundown, @@ -11,7 +12,7 @@ import { TimeStrategy, TransientEventPayload, } from 'ontime-types'; -import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils'; +import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils'; import { RUNDOWN } from '../api/constants'; import { @@ -71,6 +72,7 @@ export const useEntryActions = () => { * @private */ const _addEntryMutation = useMutation({ + // TODO(v4): optimistic create entry mutationFn: postAddEntry, onSettled: () => { queryClient.invalidateQueries({ queryKey: RUNDOWN }); @@ -83,7 +85,7 @@ export const useEntryActions = () => { */ const addEntry = useCallback( async (entry: Partial, options?: EventOptions) => { - const newEntry: TransientEventPayload = { ...entry }; + const newEntry: TransientEventPayload = { ...entry, id: generateId() }; // ************* CHECK OPTIONS specific to events if (isOntimeEvent(newEntry)) { @@ -188,6 +190,7 @@ export const useEntryActions = () => { id: previousData.id, title: previousData.title, order: previousData.order, + flatOrder: previousData.flatOrder, entries: newRundown, revision: -1, }); @@ -355,6 +358,7 @@ export const useEntryActions = () => { id: previousRundown.id, title: previousRundown.title, order: previousRundown.order, + flatOrder: previousRundown.flatOrder, entries: newRundown, revision: -1, }); @@ -399,17 +403,14 @@ export const useEntryActions = () => { if (previousData) { // optimistically update object - const newOrder = previousData.order.filter((id) => !entryIds.includes(id)); - const newRundown = { ...previousData.entries }; - for (const eventId of entryIds) { - delete newRundown[eventId]; - } + const { entries, order, flatOrder } = optimisticDeleteEntries(entryIds, previousData); queryClient.setQueryData(RUNDOWN, { id: previousData.id, title: previousData.title, - order: newOrder, - entries: newRundown, + order, + flatOrder, + entries, revision: -1, }); } @@ -462,8 +463,9 @@ export const useEntryActions = () => { queryClient.setQueryData(RUNDOWN, { id: previousData?.id ?? 'default', title: previousData?.title ?? '', - entries: {}, order: [], + flatOrder: [], + entries: {}, revision: -1, }); @@ -542,6 +544,7 @@ export const useEntryActions = () => { id: previousData.id, title: previousData.title, order: newOrder, + flatOrder: previousData.flatOrder, entries: previousData.entries, revision: -1, }); @@ -613,6 +616,7 @@ export const useEntryActions = () => { id: previousData.id, title: previousData.title, order: previousData.order, + flatOrder: previousData.flatOrder, entries: newRundown, revision: -1, }); @@ -662,3 +666,32 @@ export const useEntryActions = () => { updateCustomField, }; }; + +/** + * Utility to optimistically delete entries from client cache + */ +function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) { + const entries = { ...rundown.entries }; + let order = [...rundown.order]; + let flatOrder = [...rundown.flatOrder]; + + for (let i = 0; i < entryIds.length; i++) { + const entry = entries[entryIds[i]]; + deleteEntry(entry); + } + + function deleteEntry(entry: OntimeEntry) { + if (isOntimeEvent(entry) && entry.parent) { + const parent = entries[entry.parent] as OntimeBlock; + parent.events = parent.events.filter((event) => event !== entry.id); + parent.numEvents -= 1; + } else { + order = order.filter((id) => id !== entry.id); + } + + delete entries[entry.id]; + flatOrder = flatOrder.filter((id) => id !== entry.id); + } + + return { entries, order, flatOrder }; +} diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 3104d66f5..6ceb547ba 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -8,6 +8,7 @@ import { type Rundown, isOntimeBlock, isOntimeEvent, + OntimeEntry, Playback, SupportedEvent, } from 'ontime-types'; @@ -93,7 +94,7 @@ export default function Rundown({ data }: RundownProps) { ); const insertAtId = useCallback( - (type: SupportedEvent, id: MaybeString, above = false) => { + (patch: Partial & { type: SupportedEvent }, id: MaybeString, above = false) => { const options: EventOptions = id === null ? {} @@ -102,17 +103,10 @@ export default function Rundown({ data }: RundownProps) { before: above ? id : undefined, }; - if (type === SupportedEvent.Event) { - const newEvent = { - type: SupportedEvent.Event, - }; - if (!above && id) { - options.lastEventId = id; - } - addEntry(newEvent, options); - } else { - addEntry({ type }, options); + if (!above && id) { + options.lastEventId = id; } + addEntry(patch, options); }, [addEntry], ); @@ -208,14 +202,14 @@ export default function Rundown({ data }: RundownProps) { ['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }], - ['alt + E', () => insertAtId(SupportedEvent.Event, cursor), { preventDefault: true }], - ['alt + shift + E', () => insertAtId(SupportedEvent.Event, cursor, true), { preventDefault: true }], + ['alt + E', () => insertAtId({ type: SupportedEvent.Event }, cursor), { preventDefault: true }], + ['alt + shift + E', () => insertAtId({ type: SupportedEvent.Event }, cursor, true), { preventDefault: true }], - ['alt + B', () => insertAtId(SupportedEvent.Block, cursor), { preventDefault: true }], - ['alt + shift + B', () => insertAtId(SupportedEvent.Block, cursor, true), { preventDefault: true }], + ['alt + B', () => insertAtId({ type: SupportedEvent.Block }, cursor), { preventDefault: true }], + ['alt + shift + B', () => insertAtId({ type: SupportedEvent.Block }, cursor, true), { preventDefault: true }], - ['alt + D', () => insertAtId(SupportedEvent.Delay, cursor), { preventDefault: true }], - ['alt + shift + D', () => insertAtId(SupportedEvent.Delay, cursor, true), { preventDefault: true }], + ['alt + D', () => insertAtId({ type: SupportedEvent.Delay }, cursor), { preventDefault: true }], + ['alt + shift + D', () => insertAtId({ type: SupportedEvent.Delay }, cursor, true), { preventDefault: true }], ['mod + C', () => setEntryCopyId(cursor)], ['mod + V', () => insertCopyAtId(cursor, entryCopyId)], @@ -260,7 +254,7 @@ export default function Rundown({ data }: RundownProps) { }; if (statefulEntries.length < 1) { - return insertAtId(SupportedEvent.Event, cursor)} />; + return insertAtId({ type: SupportedEvent.Event }, cursor)} />; } // 1. gather presentation options @@ -292,15 +286,21 @@ export default function Rundown({ data }: RundownProps) { return ( {isEditMode && (hasCursor || isFirst) && ( - + )} {isOntimeBlock(entry) ? ( {entry.events.length === 0 && ( - insertAtId(SupportedEvent.Event, cursor)} /> + insertAtId({ type: SupportedEvent.Event, parent: entry.id }, entry.id)} + /> )} {entry.events.map((eventId, nestedIndex) => { const nestedEntry = entries[eventId]; + if (!nestedEntry) { + return null; + } + const nestedRundownMeta = process(nestedEntry); const isFirstInGroup = nestedIndex === 0; const isLastInGroup = nestedIndex === entry.events.length - 1; @@ -312,7 +312,10 @@ export default function Rundown({ data }: RundownProps) { return ( {isEditMode && (hasNestedCursor || isFirstInGroup) && ( - + )}
{isEditMode && (hasNestedCursor || isLastInGroup) && ( - + )}
); @@ -371,7 +374,9 @@ export default function Rundown({ data }: RundownProps) { )} - {isEditMode && (hasCursor || isLast) && } + {isEditMode && (hasCursor || isLast) && ( + + )}
); })} diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss index a3fa9aec6..f62d5555c 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss @@ -5,6 +5,7 @@ margin: 0.25rem 0; font-size: calc(1rem - 3px); + margin-left: calc(2em + 0.5rem); } .quickBtn { diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index 76edd93f4..2875307bd 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -1,74 +1,69 @@ -import { memo, useCallback, useRef } from 'react'; +import { memo, useRef } from 'react'; import { IoAdd } from 'react-icons/io5'; import { Button } from '@chakra-ui/react'; import { MaybeString, SupportedEvent } from 'ontime-types'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; -import { useEmitLog } from '../../../common/stores/logger'; import style from './QuickAddBlock.module.scss'; interface QuickAddBlockProps { previousEventId: MaybeString; - showBlocks?: boolean; + parentBlock: MaybeString; } export default memo(QuickAddBlock); function QuickAddBlock(props: QuickAddBlockProps) { - const { previousEventId, showBlocks } = props; + const { previousEventId, parentBlock } = props; const { addEntry } = useEntryActions(); - const { emitError } = useEmitLog(); const doLinkPrevious = useRef(null); const doPublic = useRef(null); - const handleCreateEvent = useCallback( - (eventType: SupportedEvent) => { - switch (eventType) { - case 'event': { - const defaultPublic = doPublic?.current?.checked; - const linkPrevious = doLinkPrevious?.current?.checked; + const addEvent = () => { + addEntry( + { + type: SupportedEvent.Event, + parent: parentBlock ?? null, + }, + { + after: previousEventId, + defaultPublic: doPublic?.current?.checked, + lastEventId: previousEventId, + linkPrevious: doLinkPrevious?.current?.checked, + }, + ); + }; - const newEvent = { type: SupportedEvent.Event }; - const options = { - after: previousEventId, - defaultPublic, - lastEventId: previousEventId, - linkPrevious, - }; - addEntry(newEvent, options); - break; - } - case 'delay': { - const options = { - lastEventId: previousEventId, - after: previousEventId, - }; - addEntry({ type: SupportedEvent.Delay }, options); - break; - } - case 'block': { - const options = { - lastEventId: previousEventId, - after: previousEventId, - }; - addEntry({ type: SupportedEvent.Block }, options); - break; - } - default: { - emitError(`Cannot create unknown event type: ${eventType}`); - break; - } - } - }, - [previousEventId, addEntry, emitError], - ); + const addDelay = () => { + addEntry( + // TODO(v4): add delays to blocks + { type: SupportedEvent.Delay }, + { + lastEventId: previousEventId, + after: previousEventId, + }, + ); + }; + + const addBlock = () => { + if (parentBlock !== null) { + return; + } + addEntry( + { type: SupportedEvent.Block }, + { + lastEventId: previousEventId, + after: previousEventId, + }, + ); + }; return (
- {showBlocks && ( + {parentBlock === null && (