mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
refactor: improve reorder logic
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
editEvent,
|
||||
ungroupEntries,
|
||||
groupEntries,
|
||||
reorderEntry,
|
||||
swapEvents,
|
||||
cloneEntry,
|
||||
} 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>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
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 { reorderEntry } from '../../services/rundown-service/RundownService.js';
|
||||
|
||||
import {
|
||||
deletesEventById,
|
||||
rundownAddToBlock,
|
||||
@@ -13,7 +19,6 @@ import {
|
||||
rundownGetCurrent,
|
||||
rundownPost,
|
||||
rundownPut,
|
||||
rundownReorder,
|
||||
rundownSwap,
|
||||
} from './rundown.controller.js';
|
||||
import {
|
||||
@@ -37,7 +42,16 @@ router.post('/', rundownPostValidator, rundownPost);
|
||||
router.put('/', rundownPutValidator, rundownPut);
|
||||
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('/applydelay/:entryId', paramsMustHaveEntryId, rundownApplyDelay);
|
||||
router.post('/clone/:entryId', paramsMustHaveEntryId, rundownCloneEntry);
|
||||
|
||||
@@ -35,9 +35,9 @@ export const rundownBatchPutValidator = [
|
||||
];
|
||||
|
||||
export const rundownReorderValidator = [
|
||||
body('eventId').isString().exists(),
|
||||
body('from').isNumeric().exists(),
|
||||
body('to').isNumeric().exists(),
|
||||
body('entryId').isString().exists(),
|
||||
body('destinationId').isString().exists(),
|
||||
body('order').isIn(['before', 'after', 'insert']).exists(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
@@ -187,13 +187,14 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
||||
|
||||
/**
|
||||
* 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 { changeList, newRundown } = await scopedMutation({ eventId, from, to });
|
||||
const { changeList, newRundown } = await scopedMutation({ entryId, destinationId, order });
|
||||
|
||||
// notify runtime that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
|
||||
@@ -783,31 +783,276 @@ describe('batchEdit() mutation', () => {
|
||||
});
|
||||
|
||||
describe('reorder() mutation', () => {
|
||||
it('should correctly reorder two events', () => {
|
||||
it('moves an event into a block', () => {
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
flatOrder: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', cue: 'data1', revision: 0 }),
|
||||
'2': makeOntimeEvent({ id: '2', cue: 'data2', revision: 0 }),
|
||||
'3': makeOntimeEvent({ id: '3', cue: 'data3', revision: 0 }),
|
||||
'1': makeOntimeBlock({ id: '1', events: [] }),
|
||||
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||
'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
|
||||
const { newRundown, changeList } = reorder({
|
||||
const { newRundown } = reorder({
|
||||
rundown: rundown,
|
||||
eventId: rundown.order[0],
|
||||
from: 0,
|
||||
to: rundown.order.length - 1,
|
||||
entryId: '1',
|
||||
destinationId: '2',
|
||||
order: 'after',
|
||||
});
|
||||
|
||||
expect(newRundown.order).toStrictEqual(['2', '3', '1']);
|
||||
expect(newRundown.entries).toMatchObject({
|
||||
'2': { id: '2', cue: 'data2', revision: 1 },
|
||||
'3': { id: '3', cue: 'data3', revision: 1 },
|
||||
'1': { id: '1', cue: 'data1', revision: 1 },
|
||||
expect(newRundown.order).toStrictEqual(['2', '1', '3']);
|
||||
// expect(newRundown.flatOrder).toStrictEqual(['2', '3', '1']);
|
||||
// expect(changeList).toStrictEqual(['2', '3', '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,
|
||||
OntimeDelay,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
generateId,
|
||||
insertAtIndex,
|
||||
reorderArray,
|
||||
swapEventData,
|
||||
customFieldLabelToKey,
|
||||
mergeAtIndex,
|
||||
} from 'ontime-utils';
|
||||
import { generateId, insertAtIndex, swapEventData, customFieldLabelToKey, mergeAtIndex } from 'ontime-utils';
|
||||
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.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 };
|
||||
}
|
||||
|
||||
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> {
|
||||
const eventFrom = rundown.entries[eventId];
|
||||
if (!eventFrom) {
|
||||
export function reorder({ rundown, entryId, destinationId, order }: ReorderArgs): Required<MutatingReturn> {
|
||||
const eventFrom = rundown.entries[entryId];
|
||||
const eventTo = rundown.entries[destinationId];
|
||||
|
||||
if (!eventFrom || !eventTo) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
rundown.order = reorderArray(rundown.order, from, to);
|
||||
|
||||
// increment revision of all events in between
|
||||
for (let i = from; i <= to; i++) {
|
||||
const eventId = rundown.order[i];
|
||||
const entry = rundown.entries[eventId];
|
||||
if (isOntimeEvent(entry) || isOntimeBlock(entry)) {
|
||||
entry.revision += 1;
|
||||
const fromParent: EntryId | null = (eventFrom as { parent?: EntryId })?.parent ?? null;
|
||||
const toParent = (() => {
|
||||
if (isOntimeBlock(eventTo)) {
|
||||
if (order === 'insert') {
|
||||
return eventTo.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return eventTo.parent ?? null;
|
||||
})();
|
||||
|
||||
if (!isOntimeBlock(eventFrom)) {
|
||||
eventFrom.parent = toParent;
|
||||
}
|
||||
|
||||
// all events from the first one, need to be updated
|
||||
const changeList = rundown.order.slice(Math.min(from, to), rundown.order.length);
|
||||
const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).events;
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user