refactor: entry actions in cuesheet

fix: insert entry before
fix: avoid double submit on enter
fix: move events in the rundown
fix: clone groups
This commit is contained in:
Carlos Valente
2025-06-30 07:16:08 +02:00
committed by Carlos Valente
parent 8ad260d28a
commit 24a3823d3b
17 changed files with 592 additions and 301 deletions
@@ -699,7 +699,7 @@ describe('rundownMutation.add()', () => {
},
});
rundownMutation.add(rundown, mockEvent, null, '1');
rundownMutation.add(rundown, mockEvent, null, rundown.entries['1'] as OntimeBlock);
expect(rundown.order).toStrictEqual(['1']);
expect(rundown.flatOrder).toStrictEqual(['1', 'mock', '1a']);
@@ -717,7 +717,7 @@ describe('rundownMutation.add()', () => {
},
});
rundownMutation.add(rundown, mockEvent, '1a', '1');
rundownMutation.add(rundown, mockEvent, '1a', rundown.entries['1'] as OntimeBlock);
expect(rundown.order).toStrictEqual(['1']);
expect(rundown.flatOrder).toStrictEqual(['1', '1a', 'mock']);
@@ -1058,6 +1058,40 @@ describe('rundownMutation.reorder()', () => {
parent: '2',
});
});
it('moves a block (up)', () => {
const rundown = makeRundown({
order: ['1', '2'],
flatOrder: ['1', '11', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
rundownMutation.reorder(rundown, rundown.entries['2'], rundown.entries['1'], 'before');
expect(rundown.order).toStrictEqual(['2', '1']);
});
it('moves a block (down)', () => {
const rundown = makeRundown({
order: ['1', '2'],
flatOrder: ['1', '11', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
rundownMutation.reorder(rundown, rundown.entries['1'], rundown.entries['2'], 'after');
expect(rundown.order).toStrictEqual(['2', '1']);
});
});
describe('rundownMutation.applyDelay()', () => {
@@ -1,10 +1,17 @@
import { TimeStrategy, EndAction, TimerType, OntimeEvent } from 'ontime-types';
import { TimeStrategy, EndAction, TimerType, OntimeEvent, OntimeBlock } from 'ontime-types';
import { MILLIS_PER_HOUR } from 'ontime-utils';
import { assertType } from 'vitest';
import { calculateDayOffset, createEvent, deleteById, doesInvalidateMetadata, getInsertAfterId, hasChanges } from '../rundown.utils.js';
import { makeRundown } from '../__mocks__/rundown.mocks.js';
import {
calculateDayOffset,
createEvent,
deleteById,
doesInvalidateMetadata,
getInsertAfterId,
hasChanges,
} from '../rundown.utils.js';
import { makeOntimeBlock, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
describe('test event validator', () => {
it('validates a good object', () => {
@@ -217,22 +224,39 @@ describe('calculateDayOffset()', () => {
describe('getInsertAfterId()', () => {
const rundown = makeRundown({
flatOrder: ['a', 'b', 'c', 'd'],
entries: {
'1': makeOntimeEvent({ id: '1', parent: null }),
'2': makeOntimeEvent({ id: '2', parent: null }),
block: makeOntimeBlock({ id: 'block', entries: ['31', '32'] }),
'31': makeOntimeEvent({ id: '31', parent: 'block' }),
'32': makeOntimeEvent({ id: '32', parent: 'block' }),
'4': makeOntimeEvent({ id: '31', parent: null }),
},
order: ['1', '2', 'block', '4'],
flatOrder: ['1', '2', 'block', '31', '32', '4'],
});
it('returns afterId if provided', () => {
expect(getInsertAfterId(rundown, 'b')).toBe('b');
expect(getInsertAfterId(rundown, null, 'b')).toBe('b');
});
it('returns the previous id before beforeId if provided', () => {
expect(getInsertAfterId(rundown, undefined, 'c')).toBe('b');
it('returns null if neither afterId nor beforeId is provided', () => {
expect(getInsertAfterId(rundown, null)).toBeNull();
});
it('returns undefined if neither afterId nor beforeId is provided', () => {
expect(getInsertAfterId(rundown)).toBeNull();
it('returns null if beforeId is not found', () => {
expect(getInsertAfterId(rundown, null, undefined, 'z')).toBeNull();
expect(getInsertAfterId(rundown, null, undefined, '1')).toBeNull();
});
it('returns undefined if beforeId is not found', () => {
expect(getInsertAfterId(rundown, undefined, 'z')).toBeNull();
it('returns the previous id of an entry in the rundown', () => {
expect(getInsertAfterId(rundown, null, undefined, '2')).toBe('1');
expect(getInsertAfterId(rundown, null, undefined, '4')).toBe('block');
expect(getInsertAfterId(rundown, null, undefined, 'block')).toBe('2');
});
it('returns the previous id of an event in a block', () => {
expect(getInsertAfterId(rundown, rundown.entries.block as OntimeBlock, undefined, '31')).toBeNull();
expect(getInsertAfterId(rundown, rundown.entries.block as OntimeBlock, undefined, '32')).toBe('31');
});
});
@@ -189,18 +189,17 @@ export function createTransaction(options: TransactionOptions): Transaction {
* - 2a. add entry to the rundown, after a given entry
* - 2b. add entry to the rundown, at the beginning
*/
function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, parentId: EntryId | null): OntimeEntry {
if (parentId) {
function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, parent: OntimeBlock | null): OntimeEntry {
if (parent) {
// 1. inserting an entry inside a block
const parentBlock = rundown.entries[parentId] as OntimeBlock;
if (afterId) {
const atEventsIndex = parentBlock.entries.indexOf(afterId) + 1;
const atEventsIndex = parent.entries.indexOf(afterId) + 1;
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
parentBlock.entries = insertAtIndex(atEventsIndex, entry.id, parentBlock.entries);
parent.entries = insertAtIndex(atEventsIndex, entry.id, parent.entries);
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
} else {
parentBlock.entries = insertAtIndex(0, entry.id, parentBlock.entries);
const atFlatIndex = rundown.flatOrder.indexOf(parentId) + 1;
parent.entries = insertAtIndex(0, entry.id, parent.entries);
const atFlatIndex = rundown.flatOrder.indexOf(parent.id) + 1;
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
}
} else {
@@ -282,6 +281,8 @@ function removeAll(rundown: Rundown): Rundown {
/**
* Reorders an entry in the rundown
* Handle moving across order lists
* @param order - 'before' | 'after' | 'insert' - where to add the entry, insert serves to add the entry into an empty block
* @throws if we insert a block inside another
*/
function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry, order: 'before' | 'after' | 'insert') {
// handle moving across parents
@@ -289,6 +290,10 @@ function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry,
const toParent = (() => {
if (isOntimeBlock(eventTo)) {
if (order === 'insert') {
// prevent blocks from being inserted into other blocks
if (isOntimeBlock(eventFrom)) {
throw new Error('Cannot insert a block into another block');
}
return eventTo.id;
}
return null;
@@ -296,7 +301,8 @@ function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry,
return eventTo.parent ?? null;
})();
if (!isOntimeBlock(eventFrom)) {
// always update the parent when moving entries
if ('parent' in eventFrom) {
eventFrom.parent = toParent;
}
@@ -469,7 +475,8 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry {
return newBlock;
} else {
return add(rundown, cloneEntry(entry, getUniqueId(rundown)), entry.id, entry.parent);
const parent: OntimeBlock | null = entry.parent ? (rundown.entries[entry.parent] as OntimeBlock) : null;
return add(rundown, cloneEntry(entry, getUniqueId(rundown)), entry.id, parent);
}
}
@@ -7,6 +7,7 @@ import {
isOntimeBlock,
isOntimeDelay,
isOntimeEvent,
OntimeBlock,
OntimeEntry,
OntimeEvent,
PatchWithId,
@@ -35,23 +36,24 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
}
// if the user provides a parent (inside a group), we make sure it exists and it is a group
let parent: EntryId | null = null;
let parent: OntimeBlock | null = null;
if ('parent' in eventData && eventData.parent != null) {
const maybeParent = rundown.entries[eventData.parent];
if (!maybeParent || !isOntimeBlock(maybeParent)) {
throw new Error(`Invalid parent event with ID ${eventData.parent}`);
}
parent = eventData.parent;
parent = maybeParent;
}
// normalise the position of the event in the rundown order
const afterId = getInsertAfterId(rundown, eventData?.after, eventData?.before);
const afterId = getInsertAfterId(rundown, parent, eventData?.after, eventData?.before);
// generate a fully formed entry from the patch
const newEntry = generateEvent(rundown, eventData, afterId);
// make mutations to rundown
rundownMutation.add(rundown, newEntry, afterId, parent);
const { rundownMetadata, revision } = commit();
// schedule the side effects
@@ -381,18 +381,25 @@ export function calculateDayOffset(
* Receives an insertion order and returns the reference to an event ID
* after which we will insert the new event
*/
export function getInsertAfterId(rundown: Rundown, afterId?: EntryId, beforeId?: EntryId): EntryId | null {
if (afterId) {
return afterId;
}
export function getInsertAfterId(
rundown: Rundown,
parent: OntimeBlock | null,
afterId?: EntryId,
beforeId?: EntryId,
): EntryId | null {
if (afterId) return afterId;
if (!beforeId) return null;
if (beforeId) {
const atIndex = rundown.flatOrder.findIndex((id) => id === beforeId);
if (atIndex < 1) return null;
return rundown.flatOrder[atIndex - 1];
}
/**
* At this point we know we want to insert before a given ID
* We need to check which list we should use to insert and find the event there
*/
const insertionList = parent ? parent.entries : rundown.order;
if (!insertionList || insertionList.length === 0) return null;
return null;
const atIndex = insertionList.findIndex((id) => id === beforeId);
if (atIndex < 1) return null;
return insertionList[atIndex - 1];
}
/**