mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
refactor: improve reorder logic
This commit is contained in:
@@ -56,9 +56,9 @@ export async function putBatchEditEvents(data: BatchEditEntry): Promise<AxiosRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
eventId: string;
|
entryId: EntryId;
|
||||||
from: number;
|
destinationId: EntryId;
|
||||||
to: number;
|
order: 'before' | 'after' | 'insert';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import {
|
|||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
TransientEventPayload,
|
TransientEventPayload,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { moveDown, moveUp } from '../../features/rundown/rundown.utils';
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
deleteEntries,
|
deleteEntries,
|
||||||
@@ -618,51 +619,6 @@ export const useEntryActions = () => {
|
|||||||
*/
|
*/
|
||||||
const _reorderEntryMutation = useMutation({
|
const _reorderEntryMutation = useMutation({
|
||||||
mutationFn: patchReorderEntry,
|
mutationFn: patchReorderEntry,
|
||||||
// we optimistically update here
|
|
||||||
onMutate: async (data) => {
|
|
||||||
// cancel ongoing queries
|
|
||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
|
||||||
|
|
||||||
// Snapshot the previous value
|
|
||||||
const previousData = queryClient.getQueryData<Rundown>(RUNDOWN);
|
|
||||||
|
|
||||||
if (previousData) {
|
|
||||||
// optimistically update object
|
|
||||||
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
|
||||||
id: previousData.id,
|
|
||||||
title: previousData.title,
|
|
||||||
order: newOrder,
|
|
||||||
flatOrder: previousData.flatOrder,
|
|
||||||
entries: previousData.entries,
|
|
||||||
revision: -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
|
||||||
return { previousData };
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
|
||||||
onError: (_error, _data, context) => {
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, context?.previousData);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mutation finished, we update the rundown with the response
|
|
||||||
onSuccess: (response) => {
|
|
||||||
if (!response.data) return;
|
|
||||||
|
|
||||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
order,
|
|
||||||
flatOrder,
|
|
||||||
entries,
|
|
||||||
revision,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -674,12 +630,12 @@ export const useEntryActions = () => {
|
|||||||
* Reorders a given entry
|
* Reorders a given entry
|
||||||
*/
|
*/
|
||||||
const reorderEntry = useCallback(
|
const reorderEntry = useCallback(
|
||||||
async (entryId: string, from: number, to: number) => {
|
async (entryId: EntryId, destinationId: EntryId, order: 'before' | 'after' | 'insert') => {
|
||||||
try {
|
try {
|
||||||
const reorderObject: ReorderEntry = {
|
const reorderObject: ReorderEntry = {
|
||||||
eventId: entryId,
|
entryId,
|
||||||
from,
|
destinationId,
|
||||||
to,
|
order,
|
||||||
};
|
};
|
||||||
await _reorderEntryMutation.mutateAsync(reorderObject);
|
await _reorderEntryMutation.mutateAsync(reorderObject);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -689,6 +645,30 @@ export const useEntryActions = () => {
|
|||||||
[_reorderEntryMutation],
|
[_reorderEntryMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const move = useCallback(async (entryId: EntryId, direction: 'up' | 'down') => {
|
||||||
|
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
if (!cachedRundown?.order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { destinationId, order } =
|
||||||
|
direction === 'up'
|
||||||
|
? moveUp(entryId, cachedRundown.order, cachedRundown.entries)
|
||||||
|
: moveDown(entryId, cachedRundown.order, cachedRundown.entries);
|
||||||
|
|
||||||
|
if (destinationId) {
|
||||||
|
try {
|
||||||
|
const reorderObject: ReorderEntry = {
|
||||||
|
entryId,
|
||||||
|
destinationId,
|
||||||
|
order: order as 'before' | 'after' | 'insert',
|
||||||
|
};
|
||||||
|
await _reorderEntryMutation.mutateAsync(reorderObject);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error re-ordering event', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to swap events
|
* Calls mutation to swap events
|
||||||
* @private
|
* @private
|
||||||
@@ -765,6 +745,7 @@ export const useEntryActions = () => {
|
|||||||
ungroup,
|
ungroup,
|
||||||
getEntryById,
|
getEntryById,
|
||||||
groupEntries,
|
groupEntries,
|
||||||
|
move,
|
||||||
reorderEntry,
|
reorderEntry,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
updateEntry,
|
updateEntry,
|
||||||
|
|||||||
@@ -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 { getNextId, getPreviousId, makeRundownMetadata, makeSortableList } from './rundown.utils';
|
import { makeRundownMetadata, makeSortableList, moveDown, moveUp } from './rundown.utils';
|
||||||
import RundownEmpty from './RundownEmpty';
|
import RundownEmpty from './RundownEmpty';
|
||||||
import { useEventSelection } from './useEventSelection';
|
import { useEventSelection } from './useEventSelection';
|
||||||
|
|
||||||
@@ -188,19 +188,26 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const moveEntry = useCallback(
|
const moveEntry = useCallback(
|
||||||
(cursor: string | null, direction: 'up' | 'down') => {
|
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||||
if (order.length < 2 || cursor == null) {
|
if (sortableData.length < 2 || cursor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationId = direction === 'up' ? getPreviousId(cursor, sortableData) : getNextId(cursor, sortableData);
|
const { destinationId, order, isBlock } =
|
||||||
if (direction === 'up' && destinationId === null) {
|
direction === 'up' ? moveUp(cursor, sortableData, entries) : moveDown(cursor, sortableData, entries);
|
||||||
reorderEntry(cursor, cursor, 'before');
|
|
||||||
} else if (destinationId !== null) {
|
if (!destinationId) {
|
||||||
reorderEntry(cursor, destinationId);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we are moving into a block, we need to make sure it is expanded
|
||||||
|
if (isBlock) {
|
||||||
|
handleCollapseGroup(false, destinationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
reorderEntry(cursor, destinationId, order as 'before' | 'after' | 'insert');
|
||||||
},
|
},
|
||||||
[order.length, sortableData, reorderEntry],
|
[sortableData, reorderEntry],
|
||||||
);
|
);
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
@@ -285,17 +292,33 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
if (over?.id) {
|
if (!over?.id || active.id === over.id) {
|
||||||
if (active.id !== over?.id) {
|
return;
|
||||||
// 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(active.id as string, over.id as string, 'before');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to specially handle the end blocks
|
||||||
|
* Dragging before and end block will add the entry to the end of the block
|
||||||
|
* Dragging after an end block will add the event after the block itself
|
||||||
|
*/
|
||||||
|
if (destinationId.startsWith('end-')) {
|
||||||
|
destinationId = destinationId.replace('end-', '');
|
||||||
|
// if we are moving before the end, we use the insert operation
|
||||||
|
order = 'insert';
|
||||||
|
}
|
||||||
|
|
||||||
|
reorderEntry(active.id as EntryId, destinationId, order);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OntimeBlock, OntimeDelay, OntimeEvent, RundownEntries, SupportedEntry } from 'ontime-types';
|
import { EntryId, OntimeBlock, OntimeDelay, OntimeEvent, RundownEntries, SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { makeRundownMetadata, makeSortableList } from '../rundown.utils';
|
import { makeRundownMetadata, makeSortableList, moveDown, moveUp } from '../rundown.utils';
|
||||||
|
|
||||||
describe('makeRundownMetadata()', () => {
|
describe('makeRundownMetadata()', () => {
|
||||||
it('processes nested rundown data', () => {
|
it('processes nested rundown data', () => {
|
||||||
@@ -333,3 +333,67 @@ describe('makeSortableList()', () => {
|
|||||||
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']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('moveUp()', () => {
|
||||||
|
const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3'];
|
||||||
|
const entries = {
|
||||||
|
event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent,
|
||||||
|
event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent,
|
||||||
|
block1: { type: 'block', id: 'block1', events: ['event3'] } as OntimeBlock,
|
||||||
|
event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent,
|
||||||
|
block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock,
|
||||||
|
event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('moves an event up in the list', () => {
|
||||||
|
const result = moveUp('event2', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'event1', order: 'before', isBlock: false });
|
||||||
|
})
|
||||||
|
|
||||||
|
it.todo('disallows nesting blocks', () => {
|
||||||
|
const result = moveUp('block2', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'block1', order: 'before', isBlock: false });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves an event into a block', () => {
|
||||||
|
const result = moveUp('event3', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'block2', order: 'insert', isBlock: true });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moving up from top is noop', () => {
|
||||||
|
const result = moveUp('event1', sortableData, entries);
|
||||||
|
expect(result).toMatchObject({ destinationId: null });
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveDown()', () => {
|
||||||
|
const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3'];
|
||||||
|
const entries = {
|
||||||
|
event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent,
|
||||||
|
event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent,
|
||||||
|
block1: { type: 'block', id: 'block1', events: ['event11'] } as OntimeBlock,
|
||||||
|
event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent,
|
||||||
|
block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock,
|
||||||
|
event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('moves an event down in the list', () => {
|
||||||
|
const result = moveDown('event1', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'event2', order: 'after', isBlock: false });
|
||||||
|
})
|
||||||
|
|
||||||
|
it.todo('disallows nesting blocks', () => {
|
||||||
|
const result = moveDown('block1', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'block2', order: 'before', isBlock: false });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves an event into a block', () => {
|
||||||
|
const result = moveDown('event2', sortableData, entries);
|
||||||
|
expect(result).toStrictEqual({ destinationId: 'event11', order: 'before', isBlock: true });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moving down from bottom is noop', () => {
|
||||||
|
const result = moveDown('event3', sortableData, entries);
|
||||||
|
expect(result).toMatchObject({ destinationId: null });
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -164,7 +164,85 @@ export function canDrop(targetType?: SupportedEntry, targetParent?: EntryId | nu
|
|||||||
return targetType == 'block';
|
return targetType == 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextId(entryId: EntryId, sortableData: EntryId[]): MaybeString {
|
/**
|
||||||
|
* Calculates destinations for an entry moving one position up in the rundown
|
||||||
|
* - Handles noops
|
||||||
|
* - Handles moving in and out of blocks
|
||||||
|
* TODO: handle moving blocks
|
||||||
|
*/
|
||||||
|
export function moveUp(entryId: EntryId, sortableData: EntryId[], entries: RundownEntries) {
|
||||||
|
const previousEntryId = getPreviousId(entryId, sortableData);
|
||||||
|
|
||||||
|
// the user is moving up at the top of the list
|
||||||
|
if (!previousEntryId) {
|
||||||
|
return { destinationId: null, order: 'before', isBlock: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousEntryId.startsWith('end-')) {
|
||||||
|
const entry = entries[entryId];
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
|
// if we are moving a block, we cannot insert it
|
||||||
|
return { destinationId: previousEntryId.replace('end-', ''), order: 'before', isBlock: false };
|
||||||
|
}
|
||||||
|
// insert in the block ID will add to the end of the block events
|
||||||
|
return { destinationId: previousEntryId.replace('end-', ''), order: 'insert', isBlock: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error -- we safeguard the entry not having a parent property
|
||||||
|
return { destinationId: previousEntryId, order: 'before', isBlock: Boolean(entries[previousEntryId]?.parent) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates destinations for an entry moving one position down in the rundown
|
||||||
|
* - Handles noops
|
||||||
|
* - Handles moving in and out of blocks
|
||||||
|
* TODO: handle moving blocks
|
||||||
|
*/
|
||||||
|
export function moveDown(entryId: EntryId, sortableData: EntryId[], entries: RundownEntries) {
|
||||||
|
const nextEntryId = getNextId(entryId, sortableData);
|
||||||
|
|
||||||
|
// the user is moving down at the end of the list
|
||||||
|
if (!nextEntryId) {
|
||||||
|
return { destinationId: null, order: 'after', isBlock: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEntryId.startsWith('end-')) {
|
||||||
|
// move outside the block
|
||||||
|
return { destinationId: nextEntryId.replace('end-', ''), order: 'after', isBlock: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the next entry is a block
|
||||||
|
* - 1. blocks need to skip over it
|
||||||
|
* - 2. if the block has children, we insert before the first child
|
||||||
|
* - 3. if the block is empty, we insert into the block
|
||||||
|
*/
|
||||||
|
if (isOntimeBlock(entries[nextEntryId])) {
|
||||||
|
const entry = entries[entryId];
|
||||||
|
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
|
// 1. if we are moving a block, we cannot insert it
|
||||||
|
return { destinationId: nextEntryId, order: 'after', isBlock: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstBlockChild = entries[nextEntryId].events.at(0);
|
||||||
|
if (firstBlockChild) {
|
||||||
|
// 2. add before the first child of the block
|
||||||
|
return { destinationId: firstBlockChild, order: 'before', isBlock: true };
|
||||||
|
} else {
|
||||||
|
// 3. or insert into an empty block
|
||||||
|
return { destinationId: nextEntryId, order: 'insert', isBlock: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { destinationId: nextEntryId, order: 'after', isBlock: Boolean(entries[nextEntryId]?.parent) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function gets the ID if the next entry in the list
|
||||||
|
* returns null if none is found
|
||||||
|
*/
|
||||||
|
function getNextId(entryId: EntryId, sortableData: EntryId[]): EntryId | null {
|
||||||
const currentIndex = sortableData.indexOf(entryId);
|
const currentIndex = sortableData.indexOf(entryId);
|
||||||
if (currentIndex === -1 || currentIndex === sortableData.length - 1) {
|
if (currentIndex === -1 || currentIndex === sortableData.length - 1) {
|
||||||
// No next ID if not found or at the end
|
// No next ID if not found or at the end
|
||||||
@@ -173,7 +251,11 @@ export function getNextId(entryId: EntryId, sortableData: EntryId[]): MaybeStrin
|
|||||||
return sortableData[currentIndex + 1];
|
return sortableData[currentIndex + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreviousId(entryId: EntryId, sortableData: EntryId[]): MaybeString {
|
/**
|
||||||
|
* Utility function gets the ID if the previous entry in the list
|
||||||
|
* returns null if none is found
|
||||||
|
*/
|
||||||
|
function getPreviousId(entryId: EntryId, sortableData: EntryId[]): EntryId | null {
|
||||||
const currentIndex = sortableData.indexOf(entryId);
|
const currentIndex = sortableData.indexOf(entryId);
|
||||||
if (currentIndex < 1) {
|
if (currentIndex < 1) {
|
||||||
// No previous ID found or at the beginning
|
// No previous ID found or at the beginning
|
||||||
|
|||||||
+3
-7
@@ -13,7 +13,7 @@ interface CuesheetTableMenuActionsProps {
|
|||||||
|
|
||||||
export default function CuesheetTableMenuActions(props: CuesheetTableMenuActionsProps) {
|
export default function CuesheetTableMenuActions(props: CuesheetTableMenuActionsProps) {
|
||||||
const { eventId, entryIndex, showModal } = props;
|
const { eventId, entryIndex, showModal } = props;
|
||||||
const { addEntry, getEntryById, reorderEntry, deleteEntry } = useEntryActions();
|
const { addEntry, getEntryById, move, deleteEntry } = useEntryActions();
|
||||||
|
|
||||||
const handleCloneEvent = () => {
|
const handleCloneEvent = () => {
|
||||||
const currentEvent = getEntryById(eventId);
|
const currentEvent = getEntryById(eventId);
|
||||||
@@ -45,14 +45,10 @@ export default function CuesheetTableMenuActions(props: CuesheetTableMenuActions
|
|||||||
Clone event
|
Clone event
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuItem
|
<MenuItem isDisabled={entryIndex < 1} icon={<IoArrowUp />} onClick={() => move(eventId, 'up')}>
|
||||||
isDisabled={entryIndex < 1}
|
|
||||||
icon={<IoArrowUp />}
|
|
||||||
onClick={() => reorderEntry(eventId, entryIndex, entryIndex - 1)}
|
|
||||||
>
|
|
||||||
Move up
|
Move up
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEntry(eventId, entryIndex, entryIndex + 1)}>
|
<MenuItem icon={<IoArrowDown />} onClick={() => move(eventId, 'down')}>
|
||||||
Move down
|
Move down
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([eventId])}>
|
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([eventId])}>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
editEvent,
|
editEvent,
|
||||||
ungroupEntries,
|
ungroupEntries,
|
||||||
groupEntries,
|
groupEntries,
|
||||||
reorderEntry,
|
|
||||||
swapEvents,
|
swapEvents,
|
||||||
cloneEntry,
|
cloneEntry,
|
||||||
} from '../../services/rundown-service/RundownService.js';
|
} from '../../services/rundown-service/RundownService.js';
|
||||||
@@ -89,21 +88,6 @@ export async function rundownBatchPut(req: Request, res: Response<MessageRespons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rundownReorder(req: Request, res: Response<Rundown | ErrorResponse>) {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { eventId, from, to } = req.body;
|
|
||||||
const newRundown = await reorderEntry(eventId, from, to);
|
|
||||||
res.status(200).send(newRundown);
|
|
||||||
} catch (error) {
|
|
||||||
const message = getErrorMessage(error);
|
|
||||||
res.status(400).send({ message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rundownSwap(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
export async function rundownSwap(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { ErrorResponse, Rundown } from 'ontime-types';
|
||||||
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
|
import { reorderEntry } from '../../services/rundown-service/RundownService.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deletesEventById,
|
deletesEventById,
|
||||||
rundownAddToBlock,
|
rundownAddToBlock,
|
||||||
@@ -13,7 +19,6 @@ import {
|
|||||||
rundownGetCurrent,
|
rundownGetCurrent,
|
||||||
rundownPost,
|
rundownPost,
|
||||||
rundownPut,
|
rundownPut,
|
||||||
rundownReorder,
|
|
||||||
rundownSwap,
|
rundownSwap,
|
||||||
} from './rundown.controller.js';
|
} from './rundown.controller.js';
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +42,16 @@ router.post('/', rundownPostValidator, rundownPost);
|
|||||||
router.put('/', rundownPutValidator, rundownPut);
|
router.put('/', rundownPutValidator, rundownPut);
|
||||||
router.put('/batch', rundownBatchPutValidator, rundownBatchPut);
|
router.put('/batch', rundownBatchPutValidator, rundownBatchPut);
|
||||||
|
|
||||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
router.patch('/reorder', rundownReorderValidator, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||||
|
try {
|
||||||
|
const { entryId, destinationId, order } = req.body;
|
||||||
|
const newRundown = await reorderEntry(entryId, destinationId, order);
|
||||||
|
res.status(200).send(newRundown);
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(400).send({ message });
|
||||||
|
}
|
||||||
|
});
|
||||||
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||||
router.patch('/applydelay/:entryId', paramsMustHaveEntryId, rundownApplyDelay);
|
router.patch('/applydelay/:entryId', paramsMustHaveEntryId, rundownApplyDelay);
|
||||||
router.post('/clone/:entryId', paramsMustHaveEntryId, rundownCloneEntry);
|
router.post('/clone/:entryId', paramsMustHaveEntryId, rundownCloneEntry);
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export const rundownBatchPutValidator = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const rundownReorderValidator = [
|
export const rundownReorderValidator = [
|
||||||
body('eventId').isString().exists(),
|
body('entryId').isString().exists(),
|
||||||
body('from').isNumeric().exists(),
|
body('destinationId').isString().exists(),
|
||||||
body('to').isNumeric().exists(),
|
body('order').isIn(['before', 'after', 'insert']).exists(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -187,13 +187,14 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* reorders a given entry
|
* reorders a given entry
|
||||||
* @param {string} eventId - ID of event from, for sanity check
|
|
||||||
* @param {number} from - index of event from
|
|
||||||
* @param {number} to - index of event to
|
|
||||||
*/
|
*/
|
||||||
export async function reorderEntry(eventId: EntryId, from: number, to: number): Promise<Rundown> {
|
export async function reorderEntry(
|
||||||
|
entryId: EntryId,
|
||||||
|
destinationId: EntryId,
|
||||||
|
order: 'before' | 'after' | 'insert',
|
||||||
|
): Promise<Rundown> {
|
||||||
const scopedMutation = cache.mutateCache(cache.reorder);
|
const scopedMutation = cache.mutateCache(cache.reorder);
|
||||||
const { changeList, newRundown } = await scopedMutation({ eventId, from, to });
|
const { changeList, newRundown } = await scopedMutation({ entryId, destinationId, order });
|
||||||
|
|
||||||
// notify runtime that rundown has changed
|
// notify runtime that rundown has changed
|
||||||
updateRuntimeOnChange();
|
updateRuntimeOnChange();
|
||||||
|
|||||||
@@ -783,31 +783,276 @@ describe('batchEdit() mutation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reorder() mutation', () => {
|
describe('reorder() mutation', () => {
|
||||||
it('should correctly reorder two events', () => {
|
it('moves an event into a block', () => {
|
||||||
const rundown = makeRundown({
|
const rundown = makeRundown({
|
||||||
order: ['1', '2', '3'],
|
order: ['1', '2', '3'],
|
||||||
|
flatOrder: ['1', '2', '3'],
|
||||||
entries: {
|
entries: {
|
||||||
'1': makeOntimeEvent({ id: '1', cue: 'data1', revision: 0 }),
|
'1': makeOntimeBlock({ id: '1', events: [] }),
|
||||||
'2': makeOntimeEvent({ id: '2', cue: 'data2', revision: 0 }),
|
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||||
'3': makeOntimeEvent({ id: '3', cue: 'data3', revision: 0 }),
|
'3': makeOntimeEvent({ id: '3', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '3',
|
||||||
|
destinationId: '1',
|
||||||
|
order: 'insert',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '3', '2']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '3', '2']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: ['3'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['3']).toMatchObject({
|
||||||
|
parent: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds an event into a block', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '11', '2'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
|
||||||
|
'11': makeOntimeEvent({ id: '11', parent: '1' }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '2',
|
||||||
|
destinationId: '11',
|
||||||
|
order: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '2', '11']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: ['2', '11'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
parent: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event after another', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2', '3'],
|
||||||
|
flatOrder: ['1', '2', '3'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', cue: 'data1' }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', cue: 'data2' }),
|
||||||
|
'3': makeOntimeEvent({ id: '3', cue: 'data3' }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// move first event to the end
|
// move first event to the end
|
||||||
const { newRundown, changeList } = reorder({
|
const { newRundown } = reorder({
|
||||||
rundown: rundown,
|
rundown: rundown,
|
||||||
eventId: rundown.order[0],
|
entryId: '1',
|
||||||
from: 0,
|
destinationId: '2',
|
||||||
to: rundown.order.length - 1,
|
order: 'after',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(newRundown.order).toStrictEqual(['2', '3', '1']);
|
expect(newRundown.order).toStrictEqual(['2', '1', '3']);
|
||||||
expect(newRundown.entries).toMatchObject({
|
// expect(newRundown.flatOrder).toStrictEqual(['2', '3', '1']);
|
||||||
'2': { id: '2', cue: 'data2', revision: 1 },
|
// expect(changeList).toStrictEqual(['2', '3', '1']);
|
||||||
'3': { id: '3', cue: 'data3', revision: 1 },
|
});
|
||||||
'1': { id: '1', cue: 'data1', revision: 1 },
|
|
||||||
|
it('moves an event before another', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2', '3'],
|
||||||
|
flatOrder: ['1', '2', '3'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', cue: 'data1' }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', cue: 'data2' }),
|
||||||
|
'3': makeOntimeEvent({ id: '3', cue: 'data3' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// move last event to the beginning
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '3',
|
||||||
|
destinationId: '1',
|
||||||
|
order: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['3', '1', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['3', '1', '2']);
|
||||||
|
// expect(changeList).toStrictEqual(['3', '1', '2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event out of a block', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '11', '2'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
|
||||||
|
'11': makeOntimeEvent({ id: '11', parent: '1' }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown, changeList } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '11',
|
||||||
|
destinationId: '2',
|
||||||
|
order: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '11', '2']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', '11', '2']);
|
||||||
|
expect(changeList).toStrictEqual(['1', '11', '2']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
parent: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event between blocks', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '11', '2', '22'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
|
||||||
|
'11': makeOntimeEvent({ id: '11', parent: '1' }),
|
||||||
|
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
|
||||||
|
'22': makeOntimeEvent({ id: '22', parent: '2' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '11',
|
||||||
|
destinationId: '22',
|
||||||
|
order: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
events: ['11', '22'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['11']).toMatchObject({
|
||||||
|
parent: '2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event into an empty block', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '2', '22'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: [] }),
|
||||||
|
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
|
||||||
|
'22': makeOntimeEvent({ id: '22', parent: '2' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '22',
|
||||||
|
destinationId: '1',
|
||||||
|
order: 'insert',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: ['22'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['22']).toMatchObject({
|
||||||
|
parent: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event out of a block (up)', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '11', '2', '22'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
|
||||||
|
'11': makeOntimeEvent({ id: '11', parent: '1' }),
|
||||||
|
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
|
||||||
|
'22': makeOntimeEvent({ id: '22', parent: '2' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '22',
|
||||||
|
destinationId: '2',
|
||||||
|
order: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '22', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: ['11'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['11']).toMatchObject({
|
||||||
|
parent: '1',
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['22']).toMatchObject({
|
||||||
|
parent: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an event out of a block (down)', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2'],
|
||||||
|
flatOrder: ['1', '11', '2', '22'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
|
||||||
|
'11': makeOntimeEvent({ id: '11', parent: '1' }),
|
||||||
|
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
|
||||||
|
'22': makeOntimeEvent({ id: '22', parent: '2' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
rundown: rundown,
|
||||||
|
entryId: '11',
|
||||||
|
destinationId: '1',
|
||||||
|
order: 'after',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', '11', '2']);
|
||||||
|
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
// expect(changeList).toStrictEqual(['1', '2', '11', '22']);
|
||||||
|
expect(rundown.entries['1']).toMatchObject({
|
||||||
|
events: [],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['11']).toMatchObject({
|
||||||
|
parent: null,
|
||||||
|
});
|
||||||
|
expect(rundown.entries['2']).toMatchObject({
|
||||||
|
events: ['22'],
|
||||||
|
});
|
||||||
|
expect(rundown.entries['22']).toMatchObject({
|
||||||
|
parent: '2',
|
||||||
});
|
});
|
||||||
expect(changeList).toStrictEqual(['2', '3', '1']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,7 @@ import {
|
|||||||
RundownEntries,
|
RundownEntries,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import { generateId, insertAtIndex, swapEventData, customFieldLabelToKey, mergeAtIndex } from 'ontime-utils';
|
||||||
generateId,
|
|
||||||
insertAtIndex,
|
|
||||||
reorderArray,
|
|
||||||
swapEventData,
|
|
||||||
customFieldLabelToKey,
|
|
||||||
mergeAtIndex,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { createBlock, createPatch } from '../../api-data/rundown/rundown.utils.js';
|
import { createBlock, createPatch } from '../../api-data/rundown/rundown.utils.js';
|
||||||
@@ -518,31 +511,61 @@ export function batchEdit({ rundown, eventIds, patch }: BatchEditArgs): Mutating
|
|||||||
return { newRundown: rundown, didMutate: true };
|
return { newRundown: rundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReorderArgs = MutationParams<{ eventId: EntryId; from: number; to: number }>;
|
type ReorderArgs = MutationParams<{
|
||||||
|
entryId: EntryId;
|
||||||
|
destinationId: EntryId;
|
||||||
|
order: 'before' | 'after' | 'insert';
|
||||||
|
}>;
|
||||||
/**
|
/**
|
||||||
* Reorder two entries
|
* Moves an event to a new position in the rundown
|
||||||
|
* Handles moving across root orders (a block order and top level order)
|
||||||
|
* @throws if entryId or destinationId not found
|
||||||
|
* @throws if trying to insert an event into a block inside another block
|
||||||
*/
|
*/
|
||||||
export function reorder({ rundown, eventId, from, to }: ReorderArgs): Required<MutatingReturn> {
|
export function reorder({ rundown, entryId, destinationId, order }: ReorderArgs): Required<MutatingReturn> {
|
||||||
const eventFrom = rundown.entries[eventId];
|
const eventFrom = rundown.entries[entryId];
|
||||||
if (!eventFrom) {
|
const eventTo = rundown.entries[destinationId];
|
||||||
|
|
||||||
|
if (!eventFrom || !eventTo) {
|
||||||
throw new Error('Event not found');
|
throw new Error('Event not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
rundown.order = reorderArray(rundown.order, from, to);
|
const fromParent: EntryId | null = (eventFrom as { parent?: EntryId })?.parent ?? null;
|
||||||
|
const toParent = (() => {
|
||||||
// increment revision of all events in between
|
if (isOntimeBlock(eventTo)) {
|
||||||
for (let i = from; i <= to; i++) {
|
if (order === 'insert') {
|
||||||
const eventId = rundown.order[i];
|
return eventTo.id;
|
||||||
const entry = rundown.entries[eventId];
|
}
|
||||||
if (isOntimeEvent(entry) || isOntimeBlock(entry)) {
|
return null;
|
||||||
entry.revision += 1;
|
|
||||||
}
|
}
|
||||||
|
return eventTo.parent ?? null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!isOntimeBlock(eventFrom)) {
|
||||||
|
eventFrom.parent = toParent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// all events from the first one, need to be updated
|
const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).events;
|
||||||
const changeList = rundown.order.slice(Math.min(from, to), rundown.order.length);
|
const destinationArray = toParent === null ? rundown.order : (rundown.entries[toParent] as OntimeBlock).events;
|
||||||
|
|
||||||
|
const fromIndex = sourceArray.indexOf(entryId);
|
||||||
|
const toIndex = (() => {
|
||||||
|
const baseIndex = destinationArray.indexOf(destinationId);
|
||||||
|
if (order === 'before') return baseIndex;
|
||||||
|
// only add one if we are moving down
|
||||||
|
if (order === 'after') return baseIndex + (fromIndex < baseIndex ? 0 : 1);
|
||||||
|
// for insert we add in the end of the array
|
||||||
|
return destinationArray.length;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Remove from source array
|
||||||
|
sourceArray.splice(fromIndex, 1);
|
||||||
|
// Insert into destination array
|
||||||
|
destinationArray.splice(toIndex, 0, entryId);
|
||||||
|
|
||||||
|
// changelist is derived from the flat order
|
||||||
|
const changeList = rundown.flatOrder.slice(Math.min(fromIndex, toIndex), rundown.flatOrder.length);
|
||||||
|
|
||||||
setIsStale();
|
|
||||||
return { newRundown: rundown, changeList, newEvent: eventFrom, didMutate: true };
|
return { newRundown: rundown, changeList, newEvent: eventFrom, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user