diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 6a4e1cc73..27001a5bf 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -42,7 +42,7 @@ import { cloneEvent } from '../../common/utils/clone'; import BlockBlock from './block-block/BlockBlock'; import BlockEnd from './block-block/BlockEnd'; import QuickAddBlock from './quick-add-block/QuickAddBlock'; -import { makeRundownMetadata, makeSortableList } from './rundown.utils'; +import { getNextId, getPreviousId, makeRundownMetadata, makeSortableList } from './rundown.utils'; import RundownEmpty from './RundownEmpty'; import { useEventSelection } from './useEventSelection'; @@ -55,10 +55,10 @@ interface RundownProps { } export default function Rundown({ data }: RundownProps) { - const { order, flatOrder, entries, id } = data; + const { order, entries, id } = data; // we create a copy of the rundown with a data structured aligned with what dnd-kit needs const featureData = useRundownEditor(); - const [sortableData, setSortableData] = useState(() => makeSortableList(flatOrder, entries)); + const [sortableData, setSortableData] = useState(() => makeSortableList(order, entries)); const [collapsedGroups, setCollapsedGroups] = useSessionStorage({ // we ensure that this is unique to the rundown key: `rundown.${id}-editor-collapsed-groups`, @@ -192,15 +192,15 @@ export default function Rundown({ data }: RundownProps) { if (order.length < 2 || cursor == null) { return; } - const { index } = - direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor); - if (index !== null) { - const offsetIndex = direction === 'up' ? index + 1 : index - 1; - reorderEntry(cursor, offsetIndex, index); + const destinationId = direction === 'up' ? getPreviousId(cursor, sortableData) : getNextId(cursor, sortableData); + if (direction === 'up' && destinationId === null) { + reorderEntry(cursor, cursor, 'before'); + } else if (destinationId !== null) { + reorderEntry(cursor, destinationId); } }, - [order, reorderEntry, entries], + [order.length, sortableData, reorderEntry], ); // shortcuts @@ -237,8 +237,8 @@ export default function Rundown({ data }: RundownProps) { // we copy the state from the store here // to workaround async updates on the drag mutations useEffect(() => { - setSortableData(makeSortableList(flatOrder, entries)); - }, [flatOrder, entries]); + setSortableData(makeSortableList(order, entries)); + }, [order, entries]); // in run mode, we follow selection useEffect(() => { @@ -287,14 +287,13 @@ export default function Rundown({ data }: RundownProps) { if (over?.id) { if (active.id !== over?.id) { - 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) => { + const fromIndex = active.data.current?.sortable.index; + const toIndex = over.data.current?.sortable.index; return reorderArray(currentEntries, fromIndex, toIndex); }); - reorderEntry(String(active.id), fromIndex, toIndex); + reorderEntry(active.id as string, over.id as string, 'before'); } } }; diff --git a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts index e91bd0149..99998e1bd 100644 --- a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts +++ b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts @@ -286,7 +286,7 @@ describe('makeRundownMetadata()', () => { describe('makeSortableList()', () => { it('generates a list with block ends', () => { - const flatOrder = ['block-1', '11', '2', 'block-3', '31', 'block-4']; + const order = ['block-1', '2', 'block-3', 'block-4']; const entries: RundownEntries = { 'block-1': { type: SupportedEntry.Block, id: 'block-1', events: ['11'] } as OntimeBlock, '11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent, @@ -296,8 +296,8 @@ describe('makeSortableList()', () => { 'block-4': { type: SupportedEntry.Block, id: 'block-4', events: [] as string[] } as OntimeBlock, }; - const sortableList = makeSortableList(flatOrder, entries); - expect(sortableList).toEqual([ + const sortableList = makeSortableList(order, entries); + expect(sortableList).toStrictEqual([ 'block-1', '11', 'end-block-1', @@ -311,25 +311,25 @@ describe('makeSortableList()', () => { }); it('closes dangling blocks', () => { - const flatOrder = ['block', '11', '12']; + const order = ['block']; const entries: RundownEntries = { block: { type: SupportedEntry.Block, id: 'block-1', events: ['11', '12'] } as OntimeBlock, '11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent, '12': { type: SupportedEntry.Event, id: '12', parent: 'block-1' } as OntimeEvent, }; - const sortableList = makeSortableList(flatOrder, entries); + const sortableList = makeSortableList(order, entries); expect(sortableList).toStrictEqual(['block-1', '11', '12', 'end-block-1']); }); it('handles a list with a with just blocks', () => { - const flatOrder = ['block-1', 'block-2']; + const order = ['block-1', 'block-2']; const entries: RundownEntries = { 'block-1': { type: SupportedEntry.Block, id: 'block-1', events: [] as string[] } as OntimeBlock, 'block-2': { type: SupportedEntry.Block, id: 'block-2', events: [] as string[] } as OntimeBlock, }; - const sortableList = makeSortableList(flatOrder, entries); + const sortableList = makeSortableList(order, entries); expect(sortableList).toStrictEqual(['block-1', 'end-block-1', 'block-2', 'end-block-2']); }); }); diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index afe66fb5c..90815173e 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -126,44 +126,29 @@ function processEntry( * Due to limitations in dnd-kit we need to flatten the list of entries * This list should also be aware of any elements that are sortable (ie: block ends) */ -export function makeSortableList(flatOrder: EntryId[], entries: RundownEntries): EntryId[] { - const entryIds: EntryId[] = []; - let lastSeenBlock: MaybeString = null; +export function makeSortableList(order: EntryId[], entries: RundownEntries): EntryId[] { + const flatIds: EntryId[] = []; - for (let i = 0; i < flatOrder.length; i++) { - const entry = entries[flatOrder[i]]; + for (let i = 0; i < order.length; i++) { + const entry = entries[order[i]]; if (!entry) { continue; } if (isOntimeBlock(entry)) { - // close any previous blocks - if (lastSeenBlock !== null) { - entryIds.push(`end-${lastSeenBlock}`); - } - lastSeenBlock = entry.id; + // inside a block there are delays and events + // there is no need for special handling + flatIds.push(entry.id); + flatIds.push(...entry.events); + + // close the block + flatIds.push(`end-${entry.id}`); + } else { + flatIds.push(entry.id); } - - if (isOntimeEvent(entry)) { - // Close the previous block if the parent changes - if (lastSeenBlock !== null && entry.parent !== lastSeenBlock) { - entryIds.push(`end-${lastSeenBlock}`); - } - lastSeenBlock = entry.parent; - } - - entryIds.push(entry.id); } - - // double check that we close any dangling blocks - // - if the last element is a block - // - if a rundown only has a top level block - if (lastSeenBlock !== null) { - entryIds.push(`end-${lastSeenBlock}`); - } - - return entryIds; + return flatIds; } /** @@ -178,3 +163,21 @@ export function canDrop(targetType?: SupportedEntry, targetParent?: EntryId | nu // we can swap places with other blocks return targetType == 'block'; } + +export function getNextId(entryId: EntryId, sortableData: EntryId[]): MaybeString { + const currentIndex = sortableData.indexOf(entryId); + if (currentIndex === -1 || currentIndex === sortableData.length - 1) { + // No next ID if not found or at the end + return null; + } + return sortableData[currentIndex + 1]; +} + +export function getPreviousId(entryId: EntryId, sortableData: EntryId[]): MaybeString { + const currentIndex = sortableData.indexOf(entryId); + if (currentIndex < 1) { + // No previous ID found or at the beginning + return null; + } + return sortableData[currentIndex - 1]; +} diff --git a/packages/utils/src/common/arrayUtils.test.ts b/packages/utils/src/common/arrayUtils.test.ts index 8f91775de..49c2442d9 100644 --- a/packages/utils/src/common/arrayUtils.test.ts +++ b/packages/utils/src/common/arrayUtils.test.ts @@ -88,12 +88,18 @@ describe('deleteAtIndex', () => { }); describe('reorderArray', () => { - it('should reorder an item in the array', () => { + it('should reorder an item in the array (up)', () => { const array = ['a', 'b', 'c', 'd']; const result = reorderArray(array, 1, 3); expect(result).toEqual(['a', 'c', 'd', 'b']); }); + it('should reorder an item in the array (down)', () => { + const array = ['a', 'b', 'c', 'd']; + const result = reorderArray(array, 3, 1); + expect(result).toEqual(['a', 'd', 'b', 'c']); + }); + it('should return the original array if fromIndex and toIndex are the same', () => { const array = ['a', 'b', 'c']; const result = reorderArray(array, 1, 1);