refactor(rundown): new entries are appended to rundown

This commit is contained in:
Carlos Valente
2026-07-18 23:44:49 +02:00
committed by Carlos Valente
parent 8363f06a5c
commit a8c611911d
16 changed files with 501 additions and 195 deletions
@@ -4,8 +4,8 @@ import type { MaybeNumber } from '../../utils/utils.type.js';
export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId };
export type InsertOptions = {
after?: EntryId;
before?: EntryId;
after?: EntryId | true;
before?: EntryId | true;
};
export type EventPostPayload = Partial<OntimeEntry> & InsertOptions;
@@ -248,28 +248,30 @@ describe('getInsertAfterId()', () => {
revision: 1,
} as Rundown;
it('returns afterId if provided', () => {
it('returns the provided after id', () => {
expect(getInsertAfterId(rundown, null, 'b')).toBe('b');
});
it('returns null if neither afterId nor beforeId is provided', () => {
it('returns null if no anchors are provided', () => {
expect(getInsertAfterId(rundown, null)).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 null if after is true', () => {
expect(getInsertAfterId(rundown, null, true)).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('group');
expect(getInsertAfterId(rundown, null, undefined, 'group')).toBe('2');
it('returns the before id if provided', () => {
expect(getInsertAfterId(rundown, null, undefined, '2')).toBe('2');
expect(getInsertAfterId(rundown, null, undefined, '4')).toBe('4');
expect(getInsertAfterId(rundown, null, undefined, 'group')).toBe('group');
});
it('returns the previous id of an event in a group', () => {
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '31')).toBeNull();
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '32')).toBe('31');
it('returns the first top-level id if before is true', () => {
expect(getInsertAfterId(rundown, null, undefined, true)).toBe('1');
});
it('returns the first group entry if before is true inside a group', () => {
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, true)).toBe('31');
});
});
@@ -331,22 +333,22 @@ describe('addToRundown()', () => {
const rundown = { id: 'test', title: '', entries: {}, order: [], flatOrder: [], revision: 0 } as Rundown;
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, null, null);
addToRundown(rundown, newEntry, null, null, null);
expect(rundown.order).toEqual(['new']);
expect(rundown.flatOrder).toEqual(['new']);
expect(rundown.entries['new']).toBe(newEntry);
});
// case 2b: insert at the beginning of the rundown
it('adds at the beginning of order and flatOrder when afterId is null', () => {
// case 2c: insert at the end of the rundown
it('adds at the end of order and flatOrder when afterId is null', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, null, null);
addToRundown(rundown, newEntry, null, null, null);
expect(rundown.order).toEqual(['new', '1', '2', 'group']);
expect(rundown.flatOrder).toEqual(['new', '1', '2', 'group', '31', '32']);
expect(rundown.order).toEqual(['1', '2', 'group', 'new']);
expect(rundown.flatOrder).toEqual(['1', '2', 'group', '31', '32', 'new']);
});
// case 2a: insert after a given entry at top level
@@ -354,19 +356,55 @@ describe('addToRundown()', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, '1', null);
addToRundown(rundown, newEntry, null, '1', null);
expect(rundown.order).toEqual(['1', 'new', '2', 'group']);
expect(rundown.flatOrder).toEqual(['1', 'new', '2', 'group', '31', '32']);
});
// case 1b: insert at the beginning of a group
it('inserts right after the group header in flatOrder and sets parent', () => {
it('prepends to the rundown when before is true', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
const afterId = getInsertAfterId(rundown, null, undefined, true);
addToRundown(rundown, newEntry, null, afterId, afterId);
expect(rundown.order).toEqual(['new', '1', '2', 'group']);
expect(rundown.flatOrder).toEqual(['new', '1', '2', 'group', '31', '32']);
});
it('inserts after a top-level group and its children in flatOrder', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, null, 'group', null);
expect(rundown.order).toEqual(['1', '2', 'group', 'new']);
expect(rundown.flatOrder).toEqual(['1', '2', 'group', '31', '32', 'new']);
});
// case 1c: insert at the end of a group
it('inserts at the end of a group and sets parent', () => {
const rundown = makeTestRundown();
const parent = rundown.entries['group'] as OntimeGroup;
const newEntry = { id: 'new', type: SupportedEntry.Event, parent: null } as OntimeEvent;
addToRundown(rundown, newEntry, null, parent);
addToRundown(rundown, newEntry, parent, null, null);
expect(parent.entries).toEqual(['31', '32', 'new']);
expect(newEntry.parent).toBe('group');
expect(rundown.flatOrder).toEqual(['1', '2', 'group', '31', '32', 'new']);
// top-level order must not change when inserting into a group
expect(rundown.order).toEqual(['1', '2', 'group']);
});
it('prepends to a group when before is true', () => {
const rundown = makeTestRundown();
const parent = rundown.entries['group'] as OntimeGroup;
const newEntry = { id: 'new', type: SupportedEntry.Event, parent: null } as OntimeEvent;
const afterId = getInsertAfterId(rundown, parent, undefined, true);
addToRundown(rundown, newEntry, parent, afterId, afterId);
expect(parent.entries).toEqual(['new', '31', '32']);
expect(newEntry.parent).toBe('group');
@@ -381,7 +419,7 @@ describe('addToRundown()', () => {
const parent = rundown.entries['group'] as OntimeGroup;
const newEntry = { id: 'new', type: SupportedEntry.Event, parent: null } as OntimeEvent;
addToRundown(rundown, newEntry, '31', parent);
addToRundown(rundown, newEntry, parent, '31', null);
expect(parent.entries).toEqual(['31', 'new', '32']);
expect(newEntry.parent).toBe('group');
@@ -361,31 +361,26 @@ export function getNextGroupNormal(
return { entry: null, index: null };
}
/**
* Receives an insertion order and returns the reference to an entry ID
* after which we will insert the new entry
*/
export function getInsertAfterId(
rundown: Rundown,
parent: OntimeGroup | null,
afterId?: EntryId,
beforeId?: EntryId,
afterId?: EntryId | true,
beforeId?: EntryId | true,
): EntryId | null {
if (afterId) return afterId;
if (!beforeId) return null;
if (beforeId) {
const insertionList = parent ? parent.entries : rundown.order;
return beforeId === true ? (insertionList[0] ?? null) : beforeId;
}
const insertionList = parent ? parent.entries : rundown.order;
if (!insertionList || insertionList.length === 0) return null;
if (afterId) return afterId === true ? null : afterId;
const atIndex = insertionList.findIndex((id) => id === beforeId);
if (atIndex < 1) return null;
return insertionList[atIndex - 1];
return null;
}
type ResolveInsertParentOptions = {
parent?: EntryId | null;
after?: EntryId;
before?: EntryId;
after?: EntryId | true;
before?: EntryId | true;
};
/**
@@ -393,15 +388,21 @@ type ResolveInsertParentOptions = {
* Uses explicit parent first, then infers from sibling references.
*/
export function resolveInsertParent(rundown: Rundown, options: ResolveInsertParentOptions): EntryId | null {
if (options.parent) {
// 1. if we have a parent reference we return that
if (options.parent !== undefined && options.parent !== null) {
return options.parent;
}
const referenceId = options.after ?? options.before;
if (!referenceId) return null;
// 2. ... otherwise we look for a sibling and get their parent
const referenceId = (() => {
if (typeof options.after === 'string') return options.after;
if (typeof options.before === 'string') return options.before;
return undefined;
})();
if (referenceId === undefined) return null;
const maybeSibling = rundown.entries[referenceId];
if (maybeSibling && 'parent' in maybeSibling && maybeSibling.parent) {
if (maybeSibling !== undefined && 'parent' in maybeSibling && maybeSibling.parent !== null) {
return maybeSibling.parent;
}
@@ -410,52 +411,57 @@ export function resolveInsertParent(rundown: Rundown, options: ResolveInsertPare
/**
* Add entry to rundown, mutates the rundown in place.
* Handles the following cases:
* - 1a. add entry in group, after a given entry
* - 1b. add entry in group, at the beginning (right after the group header)
* - 2a. add entry to the rundown, after a given entry
* - 2b. add entry to the rundown, at the beginning
* if afterId and beforeId are not provided, we add at the end of the rundown
*/
export function addToRundown(
rundown: Rundown,
entry: OntimeEntry,
afterId: EntryId | null,
parent: OntimeGroup | null,
afterId: EntryId | null,
beforeId: EntryId | null,
): OntimeEntry {
// which list to use, the top level or a group order
const insertionList = parent ? parent.entries : rundown.order;
// the index inside the list
const insertionIndex = (() => {
if (beforeId) return insertionList.indexOf(beforeId);
if (afterId) return insertionList.indexOf(afterId) + 1;
return insertionList.length;
})();
// the index inside the flat order
const flatIndex = (() => {
if (beforeId) return rundown.flatOrder.indexOf(beforeId);
if (afterId) {
const afterEntry = rundown.entries[afterId];
const flatReferenceId =
!parent && isOntimeGroup(afterEntry) && afterEntry.entries?.length > 0
? afterEntry.entries[afterEntry.entries.length - 1]
: afterId;
return rundown.flatOrder.indexOf(flatReferenceId) + 1;
}
if (parent) {
const previousId = insertionList[insertionIndex - 1] ?? parent.id;
return rundown.flatOrder.indexOf(previousId) + 1;
}
return rundown.flatOrder.length;
})();
if (parent) {
// 1. inserting an entry inside a group
// assign the parent reference on the entry
if ('parent' in entry) {
entry.parent = parent.id;
if (isOntimeGroup(entry)) {
throw new Error('Cannot add a group to another group');
}
if (afterId) {
// 1a. insert after a given entry within the group
const atEventsIndex = parent.entries.indexOf(afterId) + 1;
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
parent.entries = insertAtIndex(atEventsIndex, entry.id, parent.entries);
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
} else {
// 1b. insert at the beginning of the group (right after the group header in flatOrder)
parent.entries = insertAtIndex(0, entry.id, parent.entries);
const atFlatIndex = rundown.flatOrder.indexOf(parent.id) + 1;
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
}
entry.parent = parent.id;
parent.entries = insertAtIndex(insertionIndex, entry.id, parent.entries);
} else {
// 2. inserting an entry at top level
if (afterId) {
// 2a. insert after a given entry
const atOrderIndex = rundown.order.indexOf(afterId) + 1;
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
rundown.order = insertAtIndex(atOrderIndex, entry.id, rundown.order);
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
} else {
// 2b. insert at the beginning
rundown.order = insertAtIndex(0, entry.id, rundown.order);
rundown.flatOrder = insertAtIndex(0, entry.id, rundown.flatOrder);
}
rundown.order = insertAtIndex(insertionIndex, entry.id, rundown.order);
}
rundown.flatOrder = insertAtIndex(flatIndex, entry.id, rundown.flatOrder);
// either way, we register the entry in the entries map
rundown.entries[entry.id] = entry;