From 5777ef3532201cb779e52fda79f51c137faabca6 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Fri, 13 Feb 2026 19:59:57 +0100 Subject: [PATCH] refactor: extract parent resolution --- .../src/api-data/rundown/rundown.service.ts | 25 +++--------- packages/utils/index.ts | 1 + .../src/rundown-utils/rundownUtils.test.ts | 38 +++++++++++++++++++ .../utils/src/rundown-utils/rundownUtils.ts | 26 +++++++++++++ 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index 5ad4fb562..f4d37c24a 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -16,7 +16,7 @@ import { ProjectRundowns, InsertOptions, } from 'ontime-types'; -import { customFieldLabelToKey, getInsertAfterId } from 'ontime-utils'; +import { customFieldLabelToKey, getInsertAfterId, resolveInsertParent } from 'ontime-utils'; import { updateRundownData } from '../../stores/runtimeState.js'; import { runtimeService } from '../../services/runtime-service/runtime.service.js'; @@ -47,28 +47,15 @@ export async function addEntry(eventData: EventPostPayload): Promise { }); }); +describe('resolveInsertParent()', () => { + const rundown = { + id: 'test', + title: 'test', + entries: { + top: { id: 'top', type: SupportedEntry.Event, parent: null } as OntimeEvent, + group: { id: 'group', type: SupportedEntry.Group, entries: ['31', '32'] } as unknown as OntimeGroup, + '31': { id: '31', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent, + '32': { id: '32', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent, + }, + order: ['top', 'group'], + flatOrder: ['top', 'group', '31', '32'], + revision: 1, + } as Rundown; + + it('returns explicit parent id', () => { + expect(resolveInsertParent(rundown, { parent: 'group' })).toBe('group'); + }); + + it('returns non-existent explicit parent id as-is', () => { + expect(resolveInsertParent(rundown, { parent: 'missing' })).toBe('missing'); + }); + + it('infers parent id from sibling when parent is omitted', () => { + expect(resolveInsertParent(rundown, { after: '31' })).toBe('group'); + expect(resolveInsertParent(rundown, { before: '32' })).toBe('group'); + }); + + it('returns null when no grouped sibling reference exists', () => { + expect(resolveInsertParent(rundown, { after: 'top' })).toBeNull(); + }); + + it('returns null when no references are provided', () => { + expect(resolveInsertParent(rundown, {})).toBeNull(); + }); +}); + describe('addToRundown()', () => { const makeTestRundown = (): Rundown => ({ diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts index 9ee86de8e..90d6aa658 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -382,6 +382,32 @@ export function getInsertAfterId( return insertionList[atIndex - 1]; } +type ResolveInsertParentOptions = { + parent?: EntryId | null; + after?: EntryId; + before?: EntryId; +}; + +/** + * Resolves the parent ID for an insertion. + * Uses explicit parent first, then infers from sibling references. + */ +export function resolveInsertParent(rundown: Rundown, options: ResolveInsertParentOptions): EntryId | null { + if (options.parent) { + return options.parent; + } + + const referenceId = options.after ?? options.before; + if (!referenceId) return null; + + const maybeSibling = rundown.entries[referenceId]; + if (maybeSibling && 'parent' in maybeSibling && maybeSibling.parent) { + return maybeSibling.parent; + } + + return null; +} + /** * Add entry to rundown, mutates the rundown in place. * Handles the following cases: