From 524721002a1a39456133b49c3ae3629241297932 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 27 Jul 2025 17:05:15 +0200 Subject: [PATCH] fix: prevent nesting groups --- .../client/src/common/hooks/useEntryAction.ts | 1 + apps/client/src/features/rundown/Rundown.tsx | 23 +++++++++++++------ .../rundown/rundown-block/BlockEnd.tsx | 3 +++ .../src/features/rundown/rundown.utils.ts | 20 ++++++++++------ 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apps/client/src/common/hooks/useEntryAction.ts b/apps/client/src/common/hooks/useEntryAction.ts index 965614758..ffbe40335 100644 --- a/apps/client/src/common/hooks/useEntryAction.ts +++ b/apps/client/src/common/hooks/useEntryAction.ts @@ -693,6 +693,7 @@ export const useEntryActions = () => { await reorderEntryMutation(reorderObject); } catch (error) { logAxiosError('Error re-ordering event', error); + throw error; // rethrow to handle in the component } }, [reorderEntryMutation], diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 89e0a60ef..9f8fd5b82 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -44,7 +44,7 @@ import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons'; import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline'; import BlockEnd from './rundown-block/BlockEnd'; import RundownBlock from './rundown-block/RundownBlock'; -import { makeRundownMetadata, makeSortableList } from './rundown.utils'; +import { canDrop, makeRundownMetadata, makeSortableList } from './rundown.utils'; import RundownEmpty from './RundownEmpty'; import { useEventSelection } from './useEventSelection'; @@ -335,14 +335,14 @@ export default function Rundown({ data }: RundownProps) { return; } + // prevent dropping a group inside another + if (active.data.current?.type === 'block' && !canDrop(over.data.current?.type, over.data.current?.parent)) { + return; + } + const fromIndex = active.data.current?.sortable.index; const toIndex = over.data.current?.sortable.index; - // we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates - setSortableData((currentEntries) => { - return reorderArray(currentEntries, fromIndex, toIndex); - }); - let destinationId = over.id as EntryId; let order: 'before' | 'after' | 'insert' = fromIndex < toIndex ? 'after' : 'before'; @@ -368,7 +368,16 @@ export default function Rundown({ data }: RundownProps) { } } } - reorderEntry(active.id as EntryId, destinationId, order); + + // keep copy of the current state in case we need to revert + const currentEntries = structuredClone(sortableData); + // we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates + setSortableData((currentEntries) => { + return reorderArray(currentEntries, fromIndex, toIndex); + }); + reorderEntry(active.id as EntryId, destinationId, order).catch((_) => { + setSortableData(currentEntries); + }); }; /** diff --git a/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx b/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx index b3bd9ebfb..c3a2ed788 100644 --- a/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx +++ b/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx @@ -17,6 +17,9 @@ export default function BlockEnd({ id, colour }: BlockEndProps) { transition, } = useSortable({ id, + data: { + type: 'end-block', + }, animateLayoutChanges: () => false, disabled: true, // we do not want to drag end blocks }); diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index 368f400fb..19d07cdaa 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -158,15 +158,21 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent /** * Checks whether a drop operation is valid - * Currently only used for validating dropping blocks + * Currently only used for validating dropping groups */ -export function canDrop(targetType?: SupportedEntry, targetParent?: EntryId | null): boolean { - if (targetType === 'event' || targetType === 'delay') { - return targetParent === null; +export function canDrop(targetType?: SupportedEntry & 'end-block', targetParent?: EntryId | null): boolean { + // this would mean inserting a group inside another + if (targetType === 'end-block') { + return false; } - // remaining events will be block or end-block - // we can swap places with other blocks - return targetType == 'block'; + + // this means swapping places with another group + if (targetType === 'block') { + return true; + } + + // for all other cases, we just need to check if we are inside a group + return targetParent === null; } /**