mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
refactor: order is single source of truth
This commit is contained in:
committed by
Carlos Valente
parent
e7cfb7d9d9
commit
2793aadea0
@@ -42,7 +42,7 @@ import { cloneEvent } from '../../common/utils/clone';
|
|||||||
import BlockBlock from './block-block/BlockBlock';
|
import BlockBlock from './block-block/BlockBlock';
|
||||||
import BlockEnd from './block-block/BlockEnd';
|
import BlockEnd from './block-block/BlockEnd';
|
||||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
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 RundownEmpty from './RundownEmpty';
|
||||||
import { useEventSelection } from './useEventSelection';
|
import { useEventSelection } from './useEventSelection';
|
||||||
|
|
||||||
@@ -55,10 +55,10 @@ interface RundownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown({ data }: 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
|
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const [sortableData, setSortableData] = useState<EntryId[]>(() => makeSortableList(flatOrder, entries));
|
const [sortableData, setSortableData] = useState<EntryId[]>(() => makeSortableList(order, entries));
|
||||||
const [collapsedGroups, setCollapsedGroups] = useSessionStorage<EntryId[]>({
|
const [collapsedGroups, setCollapsedGroups] = useSessionStorage<EntryId[]>({
|
||||||
// we ensure that this is unique to the rundown
|
// we ensure that this is unique to the rundown
|
||||||
key: `rundown.${id}-editor-collapsed-groups`,
|
key: `rundown.${id}-editor-collapsed-groups`,
|
||||||
@@ -192,15 +192,15 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
if (order.length < 2 || cursor == null) {
|
if (order.length < 2 || cursor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { index } =
|
|
||||||
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
|
|
||||||
|
|
||||||
if (index !== null) {
|
const destinationId = direction === 'up' ? getPreviousId(cursor, sortableData) : getNextId(cursor, sortableData);
|
||||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
if (direction === 'up' && destinationId === null) {
|
||||||
reorderEntry(cursor, offsetIndex, index);
|
reorderEntry(cursor, cursor, 'before');
|
||||||
|
} else if (destinationId !== null) {
|
||||||
|
reorderEntry(cursor, destinationId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, reorderEntry, entries],
|
[order.length, sortableData, reorderEntry],
|
||||||
);
|
);
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
@@ -237,8 +237,8 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// we copy the state from the store here
|
// we copy the state from the store here
|
||||||
// to workaround async updates on the drag mutations
|
// to workaround async updates on the drag mutations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSortableData(makeSortableList(flatOrder, entries));
|
setSortableData(makeSortableList(order, entries));
|
||||||
}, [flatOrder, entries]);
|
}, [order, entries]);
|
||||||
|
|
||||||
// in run mode, we follow selection
|
// in run mode, we follow selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -287,14 +287,13 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
if (over?.id) {
|
if (over?.id) {
|
||||||
if (active.id !== 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
|
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||||
setSortableData((currentEntries) => {
|
setSortableData((currentEntries) => {
|
||||||
|
const fromIndex = active.data.current?.sortable.index;
|
||||||
|
const toIndex = over.data.current?.sortable.index;
|
||||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||||
});
|
});
|
||||||
reorderEntry(String(active.id), fromIndex, toIndex);
|
reorderEntry(active.id as string, over.id as string, 'before');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
|
|
||||||
describe('makeSortableList()', () => {
|
describe('makeSortableList()', () => {
|
||||||
it('generates a list with block ends', () => {
|
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 = {
|
const entries: RundownEntries = {
|
||||||
'block-1': { type: SupportedEntry.Block, id: 'block-1', events: ['11'] } as OntimeBlock,
|
'block-1': { type: SupportedEntry.Block, id: 'block-1', events: ['11'] } as OntimeBlock,
|
||||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
|
'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,
|
'block-4': { type: SupportedEntry.Block, id: 'block-4', events: [] as string[] } as OntimeBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortableList = makeSortableList(flatOrder, entries);
|
const sortableList = makeSortableList(order, entries);
|
||||||
expect(sortableList).toEqual([
|
expect(sortableList).toStrictEqual([
|
||||||
'block-1',
|
'block-1',
|
||||||
'11',
|
'11',
|
||||||
'end-block-1',
|
'end-block-1',
|
||||||
@@ -311,25 +311,25 @@ describe('makeSortableList()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes dangling blocks', () => {
|
it('closes dangling blocks', () => {
|
||||||
const flatOrder = ['block', '11', '12'];
|
const order = ['block'];
|
||||||
const entries: RundownEntries = {
|
const entries: RundownEntries = {
|
||||||
block: { type: SupportedEntry.Block, id: 'block-1', events: ['11', '12'] } as OntimeBlock,
|
block: { type: SupportedEntry.Block, id: 'block-1', events: ['11', '12'] } as OntimeBlock,
|
||||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
|
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
|
||||||
'12': { type: SupportedEntry.Event, id: '12', 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']);
|
expect(sortableList).toStrictEqual(['block-1', '11', '12', 'end-block-1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles a list with a with just blocks', () => {
|
it('handles a list with a with just blocks', () => {
|
||||||
const flatOrder = ['block-1', 'block-2'];
|
const order = ['block-1', 'block-2'];
|
||||||
const entries: RundownEntries = {
|
const entries: RundownEntries = {
|
||||||
'block-1': { type: SupportedEntry.Block, id: 'block-1', events: [] as string[] } as OntimeBlock,
|
'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,
|
'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']);
|
expect(sortableList).toStrictEqual(['block-1', 'end-block-1', 'block-2', 'end-block-2']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,44 +126,29 @@ function processEntry(
|
|||||||
* Due to limitations in dnd-kit we need to flatten the list of entries
|
* 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)
|
* This list should also be aware of any elements that are sortable (ie: block ends)
|
||||||
*/
|
*/
|
||||||
export function makeSortableList(flatOrder: EntryId[], entries: RundownEntries): EntryId[] {
|
export function makeSortableList(order: EntryId[], entries: RundownEntries): EntryId[] {
|
||||||
const entryIds: EntryId[] = [];
|
const flatIds: EntryId[] = [];
|
||||||
let lastSeenBlock: MaybeString = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < flatOrder.length; i++) {
|
for (let i = 0; i < order.length; i++) {
|
||||||
const entry = entries[flatOrder[i]];
|
const entry = entries[order[i]];
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOntimeBlock(entry)) {
|
if (isOntimeBlock(entry)) {
|
||||||
// close any previous blocks
|
// inside a block there are delays and events
|
||||||
if (lastSeenBlock !== null) {
|
// there is no need for special handling
|
||||||
entryIds.push(`end-${lastSeenBlock}`);
|
flatIds.push(entry.id);
|
||||||
}
|
flatIds.push(...entry.events);
|
||||||
lastSeenBlock = entry.id;
|
|
||||||
|
// 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);
|
|
||||||
}
|
}
|
||||||
|
return flatIds;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,3 +163,21 @@ export function canDrop(targetType?: SupportedEntry, targetParent?: EntryId | nu
|
|||||||
// we can swap places with other blocks
|
// we can swap places with other blocks
|
||||||
return targetType == 'block';
|
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];
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,12 +88,18 @@ describe('deleteAtIndex', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reorderArray', () => {
|
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 array = ['a', 'b', 'c', 'd'];
|
||||||
const result = reorderArray(array, 1, 3);
|
const result = reorderArray(array, 1, 3);
|
||||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
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', () => {
|
it('should return the original array if fromIndex and toIndex are the same', () => {
|
||||||
const array = ['a', 'b', 'c'];
|
const array = ['a', 'b', 'c'];
|
||||||
const result = reorderArray(array, 1, 1);
|
const result = reorderArray(array, 1, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user