mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
refactor: allow secondary rundowns (#2086)
* refactor: allow secondary rundowns * ui: move dropdown * feat: follow loaded * ui: background edit warning ui: fix disable radio button * feat(ui): add loaded sufix in the rundown list * feat(ui): add direct link to background edit from rundown manager * fix: better fallback * fix: default to is isCurrentRundown for nav bar colour * chore: rename navigate to cuesheet --------- Co-authored-by: alex-arc <ac@omnivox.dk>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
CustomField,
|
||||
EntryId,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeGroup,
|
||||
OntimeMilestone,
|
||||
Rundown,
|
||||
SupportedEntry,
|
||||
isOntimeGroup,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { makeNewRundown } from '../../../models/dataModel.js';
|
||||
@@ -59,13 +61,24 @@ export function makeOntimeMilestone(patch: Partial<OntimeMilestone>): OntimeMile
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create a rundown object
|
||||
* Utility to create a rundown object.
|
||||
* Derives flatOrder from order/entries when the patch does not provide one,
|
||||
* so test fixtures match the shape of a processed rundown.
|
||||
*/
|
||||
export function makeRundown(patch: Partial<Rundown>): Rundown {
|
||||
return {
|
||||
...makeNewRundown(),
|
||||
...patch,
|
||||
};
|
||||
const rundown = { ...makeNewRundown(), ...patch };
|
||||
if (!patch.flatOrder) {
|
||||
const flatOrder: EntryId[] = [];
|
||||
for (const id of rundown.order) {
|
||||
flatOrder.push(id);
|
||||
const entry = rundown.entries[id];
|
||||
if (isOntimeGroup(entry)) {
|
||||
flatOrder.push(...entry.entries);
|
||||
}
|
||||
}
|
||||
rundown.flatOrder = flatOrder;
|
||||
}
|
||||
return rundown;
|
||||
}
|
||||
|
||||
export function makeCustomField(patch: Partial<CustomField>): CustomField {
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('createTransaction', () => {
|
||||
expect(typeof commit).toBe('function');
|
||||
});
|
||||
|
||||
it('should return the updated data after commit is called and writes are scheduled', () => {
|
||||
it('should return the updated data after commit is called and writes are scheduled', async () => {
|
||||
const { rundown, customFields, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: true });
|
||||
rundown.title = 'Another Title';
|
||||
customFields['newField'] = {
|
||||
@@ -72,8 +72,7 @@ describe('createTransaction', () => {
|
||||
colour: 'blue',
|
||||
};
|
||||
|
||||
const updated = commit();
|
||||
vi.runAllTimers();
|
||||
const updated = await commit();
|
||||
|
||||
expect(updated.rundown.title).toBe('Another Title');
|
||||
expect(updated.customFields).toHaveProperty('newField');
|
||||
@@ -132,6 +131,23 @@ describe('processRundown()', () => {
|
||||
expect(initResult.entries['3'].type).toBe(SupportedEntry.Delay);
|
||||
});
|
||||
|
||||
it('produces order, flatOrder and applies delays to following events', () => {
|
||||
const rundown = makeRundown({
|
||||
order: ['1', 'delay', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 1000, duration: 1000 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 250 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 1000, timeEnd: 2000, duration: 1000 }),
|
||||
},
|
||||
});
|
||||
|
||||
const processed = processRundown(rundown, {});
|
||||
|
||||
expect(processed.order).toEqual(['1', 'delay', '2']);
|
||||
expect(processed.flatEntryOrder).toEqual(['1', 'delay', '2']);
|
||||
expect((processed.entries['2'] as OntimeEvent).delay).toBe(250);
|
||||
});
|
||||
|
||||
it('calculates delays versions of a given rundown', () => {
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2'],
|
||||
@@ -1622,6 +1638,34 @@ describe('rundownMutation.applyDelay()', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the passed rundown own order, not the loaded rundown metadata', () => {
|
||||
// The loaded rundown has completely different events
|
||||
const loadedRundown = makeRundown({
|
||||
order: ['loaded-delay', 'loaded-ev'],
|
||||
entries: {
|
||||
'loaded-delay': makeOntimeDelay({ id: 'loaded-delay', duration: 9999 }),
|
||||
'loaded-ev': makeOntimeEvent({ id: 'loaded-ev', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
},
|
||||
});
|
||||
rundownCache.init(loadedRundown, {});
|
||||
|
||||
// Background rundown is independent of the loaded one
|
||||
const bgRundown = makeRundown({
|
||||
order: ['bg-delay', 'bg-ev'],
|
||||
entries: {
|
||||
'bg-delay': makeOntimeDelay({ id: 'bg-delay', duration: 500 }),
|
||||
'bg-ev': makeOntimeEvent({ id: 'bg-ev', timeStart: 0, timeEnd: 200, duration: 200 }),
|
||||
},
|
||||
});
|
||||
|
||||
rundownMutation.applyDelay(bgRundown, bgRundown.entries['bg-delay'] as OntimeDelay);
|
||||
|
||||
// bg-ev must be shifted by its own delay (500), not influenced by the loaded rundown
|
||||
expect((bgRundown.entries['bg-ev'] as OntimeEvent).timeStart).toBe(500);
|
||||
// loaded-ev must be untouched
|
||||
expect((loadedRundown.entries['loaded-ev'] as OntimeEvent).timeStart).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rundownMutation.swap()', () => {
|
||||
|
||||
@@ -82,56 +82,85 @@ export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cac
|
||||
type Transaction = {
|
||||
customFields: CustomFields;
|
||||
rundown: Rundown;
|
||||
rundownMetadata: Readonly<RundownMetadata>;
|
||||
|
||||
commit: (shouldProcess?: boolean) => {
|
||||
commit: (shouldProcess?: boolean) => Promise<{
|
||||
rundown: Readonly<Rundown>;
|
||||
rundownMetadata: Readonly<RundownMetadata>;
|
||||
rundownMetadata: Readonly<RundownMetadata> | null;
|
||||
customFields: Readonly<CustomFields>;
|
||||
revision: Readonly<number>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
type TransactionOptions = {
|
||||
mutableRundown?: boolean;
|
||||
mutableCustomFields?: boolean;
|
||||
/**
|
||||
* Target rundown ID. Defaults to the loaded rundown.
|
||||
* When targeting a non-loaded ("background") rundown, the cache and runtime
|
||||
* metadata are bypassed — the rundown is read from disk and persisted directly.
|
||||
*/
|
||||
rundownId?: string;
|
||||
};
|
||||
|
||||
export function createTransaction(options: TransactionOptions): Transaction {
|
||||
const rundown = options.mutableRundown ? structuredClone(cachedRundown) : cachedRundown;
|
||||
const targetId = options.rundownId ?? cachedRundown.id;
|
||||
const isLoaded = targetId === cachedRundown.id;
|
||||
const sourceRundown: Rundown = isLoaded ? cachedRundown : (getDataProvider().getRundown(targetId) as Rundown);
|
||||
const rundown = options.mutableRundown ? structuredClone(sourceRundown) : sourceRundown;
|
||||
const customFields = options.mutableCustomFields ? structuredClone(projectCustomFields) : projectCustomFields;
|
||||
|
||||
/**
|
||||
* Applies a mutated rundown to the cache
|
||||
* Applies a mutated rundown to its persistence target
|
||||
* @param shouldProcess - whether the rundown should be processed after the commit
|
||||
* Some edit mutations, and custom field changes do not require processing
|
||||
*/
|
||||
function commit(shouldProcess: boolean = true) {
|
||||
async function commit(shouldProcess: boolean = true) {
|
||||
let committedRundown: Readonly<Rundown> = isLoaded ? cachedRundown : rundown;
|
||||
let committedMetadata: RundownMetadata | null = isLoaded ? rundownMetadata : null;
|
||||
let committedRevision: number = sourceRundown.revision;
|
||||
|
||||
// if the rundown is mutable we persist the changes
|
||||
if (options.mutableRundown) {
|
||||
// update fields which are agnostic of whether the rundown is processed
|
||||
cachedRundown.revision = cachedRundown.revision + 1;
|
||||
cachedRundown.title = rundown.title;
|
||||
if (isLoaded) {
|
||||
// update fields which are agnostic of whether the rundown is processed
|
||||
cachedRundown.revision = cachedRundown.revision + 1;
|
||||
cachedRundown.title = rundown.title;
|
||||
|
||||
if (!shouldProcess) {
|
||||
// if we dont need to process, we just reassign the commit data to the cache
|
||||
cachedRundown.entries = rundown.entries;
|
||||
cachedRundown.order = rundown.order;
|
||||
cachedRundown.flatOrder = rundown.flatOrder;
|
||||
if (!shouldProcess) {
|
||||
// if we dont need to process, we just reassign the commit data to the cache
|
||||
cachedRundown.entries = rundown.entries;
|
||||
cachedRundown.order = rundown.order;
|
||||
cachedRundown.flatOrder = rundown.flatOrder;
|
||||
} else {
|
||||
const processedData = processRundown(rundown, projectCustomFields, { mutate: true });
|
||||
// update the cache values
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data
|
||||
const { previousEvent, latestEvent, previousEntry, entries, order, ...metadata } = processedData;
|
||||
|
||||
cachedRundown.entries = entries;
|
||||
cachedRundown.order = order;
|
||||
cachedRundown.flatOrder = metadata.flatEntryOrder;
|
||||
rundownMetadata = metadata;
|
||||
}
|
||||
|
||||
// persist after all mutations are applied
|
||||
await getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
||||
committedRundown = cachedRundown;
|
||||
committedMetadata = rundownMetadata;
|
||||
committedRevision = cachedRundown.revision;
|
||||
} else {
|
||||
const processedData = processRundown(rundown, projectCustomFields);
|
||||
// update the cache values
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data
|
||||
const { previousEvent, latestEvent, previousEntry, entries, order, ...metadata } = processedData;
|
||||
|
||||
cachedRundown.entries = entries;
|
||||
cachedRundown.order = order;
|
||||
cachedRundown.flatOrder = metadata.flatEntryOrder;
|
||||
rundownMetadata = metadata;
|
||||
// background rundown: process if requested, persist directly, no runtime metadata
|
||||
rundown.revision = rundown.revision + 1;
|
||||
if (shouldProcess) {
|
||||
const processedData = processRundown(rundown, projectCustomFields, { mutate: true });
|
||||
rundown.entries = processedData.entries;
|
||||
rundown.order = processedData.order;
|
||||
rundown.flatOrder = processedData.flatEntryOrder;
|
||||
}
|
||||
await getDataProvider().setRundown(targetId, rundown);
|
||||
committedRundown = rundown;
|
||||
committedRevision = rundown.revision;
|
||||
}
|
||||
|
||||
// persist after all mutations are applied
|
||||
getDataProvider().setRundown(cachedRundown.id, cachedRundown);
|
||||
}
|
||||
|
||||
// if the customFields are mutable we persist the changes
|
||||
@@ -139,21 +168,20 @@ export function createTransaction(options: TransactionOptions): Transaction {
|
||||
projectCustomFields = customFields;
|
||||
|
||||
// persist after reassignment
|
||||
getDataProvider().setCustomFields(projectCustomFields);
|
||||
await getDataProvider().setCustomFields(projectCustomFields);
|
||||
}
|
||||
|
||||
return {
|
||||
rundown: cachedRundown,
|
||||
rundownMetadata,
|
||||
rundown: committedRundown,
|
||||
rundownMetadata: committedMetadata,
|
||||
customFields: projectCustomFields,
|
||||
revision: cachedRundown.revision,
|
||||
revision: committedRevision,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
customFields,
|
||||
rundown,
|
||||
rundownMetadata,
|
||||
commit,
|
||||
};
|
||||
}
|
||||
@@ -288,11 +316,11 @@ function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry,
|
||||
* Mutates the given rundown
|
||||
*/
|
||||
function applyDelay(rundown: Rundown, delay: OntimeDelay) {
|
||||
const delayIndex = rundownMetadata.flatEntryOrder.indexOf(delay.id);
|
||||
const delayIndex = rundown.flatOrder.indexOf(delay.id);
|
||||
|
||||
// if the delay is empty, or the last element
|
||||
// there is nothing do apply
|
||||
if (delay.duration === 0 || delayIndex === rundownMetadata.flatEntryOrder.length - 1) {
|
||||
if (delay.duration === 0 || delayIndex === rundown.flatOrder.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -305,8 +333,8 @@ function applyDelay(rundown: Rundown, delay: OntimeDelay) {
|
||||
let lastEntry: OntimeEvent | null = null;
|
||||
let isFirstEvent = true;
|
||||
|
||||
for (let i = delayIndex + 1; i < rundownMetadata.flatEntryOrder.length; i++) {
|
||||
const currentId = rundownMetadata.flatEntryOrder[i];
|
||||
for (let i = delayIndex + 1; i < rundown.flatOrder.length; i++) {
|
||||
const currentId = rundown.flatOrder[i];
|
||||
const currentEntry = rundown.entries[currentId];
|
||||
|
||||
// we don't do operation on other event types
|
||||
@@ -588,6 +616,23 @@ export async function updateBackgroundRundown(rundownId: string, rundown: Rundow
|
||||
await getDataProvider().setRundown(rundownId, rundown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a rundown from disk and returns it in the processed shape used by clients.
|
||||
* Used for non-loaded rundowns — the loaded rundown is served directly from cache.
|
||||
*/
|
||||
export function getProcessedRundown(rundownId: string): Rundown {
|
||||
const stored = getDataProvider().getRundown(rundownId);
|
||||
const processed = processRundown(stored, getDataProvider().getCustomFields());
|
||||
return {
|
||||
id: stored.id,
|
||||
title: stored.title,
|
||||
entries: processed.entries,
|
||||
order: processed.order,
|
||||
flatOrder: processed.flatEntryOrder,
|
||||
revision: stored.revision,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new custom field to the object and returns it
|
||||
*/
|
||||
@@ -666,7 +711,7 @@ export const customFieldMutation = {
|
||||
export function init(initialRundown: Readonly<Rundown>, initialCustomFields: Readonly<CustomFields>) {
|
||||
const rundown = structuredClone(initialRundown);
|
||||
const customFields = structuredClone(initialCustomFields);
|
||||
const processedData = processRundown(rundown, customFields);
|
||||
const processedData = processRundown(rundown, customFields, { mutate: true });
|
||||
|
||||
// update the cache values
|
||||
cachedRundown.id = rundown.id;
|
||||
@@ -699,72 +744,57 @@ export const rundownCache = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility updates cache after a mutation
|
||||
* Handles calculating the rundown metadata
|
||||
* Computes derived rundown metadata (delays, gaps, group times, ordered lists).
|
||||
* By default the input rundown is not mutated — entries are cloned as they are processed.
|
||||
* Callers that own a writable rundown can pass `{ mutate: true }` to skip the per-entry clone.
|
||||
* @private should not be called outside of `rundown.dao.ts`, exported for testing
|
||||
*/
|
||||
export function processRundown(
|
||||
initialRundown: Readonly<Rundown>,
|
||||
customFields: Readonly<CustomFields>,
|
||||
options?: { mutate?: boolean },
|
||||
): ProcessedRundownMetadata {
|
||||
const { process, getMetadata } = makeRundownMetadata(customFields);
|
||||
const { process, getMetadata } = makeRundownMetadata(customFields, options);
|
||||
|
||||
for (let i = 0; i < initialRundown.order.length; i++) {
|
||||
// we assign a reference to the current entry, this will be mutated in place
|
||||
const currentEntryId = initialRundown.order[i];
|
||||
const currentEntry = initialRundown.entries[currentEntryId];
|
||||
if (!currentEntry) {
|
||||
continue;
|
||||
}
|
||||
const { processedEntry } = process(currentEntry, null);
|
||||
const currentEntry = initialRundown.entries[initialRundown.order[i]];
|
||||
if (!currentEntry) continue;
|
||||
|
||||
// if the event is a group, we process the nested entries
|
||||
// the code here is a copy of the processing of top level events
|
||||
if (isOntimeGroup(processedEntry)) {
|
||||
let groupStartTime = null;
|
||||
let groupEndTime = null;
|
||||
let isFirstLinked = false;
|
||||
const groupEvents: EntryId[] = [];
|
||||
processedEntry.duration = 0;
|
||||
const processedEntry = process(currentEntry, null);
|
||||
if (!isOntimeGroup(processedEntry)) continue;
|
||||
|
||||
// check if the group contains nested entries
|
||||
for (let j = 0; j < processedEntry.entries.length; j++) {
|
||||
const nestedEntryId = processedEntry.entries[j];
|
||||
const nestedEntry = initialRundown.entries[nestedEntryId];
|
||||
// process nested entries and recompute aggregate group fields
|
||||
let groupStartTime: number | null = null;
|
||||
let groupEndTime: number | null = null;
|
||||
let isFirstLinked = false;
|
||||
const groupEvents: EntryId[] = [];
|
||||
processedEntry.duration = 0;
|
||||
|
||||
if (!nestedEntry) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < processedEntry.entries.length; j++) {
|
||||
const nestedEntry = initialRundown.entries[processedEntry.entries[j]];
|
||||
if (!nestedEntry) continue;
|
||||
|
||||
groupEvents.push(nestedEntry.id);
|
||||
const { processedEntry: processedNestedEntry } = process(nestedEntry, processedEntry.id);
|
||||
groupEvents.push(nestedEntry.id);
|
||||
const processedNestedEntry = process(nestedEntry, processedEntry.id);
|
||||
|
||||
// we dont extract metadata of skipped events,
|
||||
// if this is not a playable event there is nothing else to do
|
||||
if (!isOntimeEvent(processedNestedEntry) || !isPlayableEvent(processedNestedEntry)) {
|
||||
continue;
|
||||
}
|
||||
// skip metadata aggregation for non-playable nested entries
|
||||
if (!isOntimeEvent(processedNestedEntry) || !isPlayableEvent(processedNestedEntry)) continue;
|
||||
|
||||
// first start is always the first event
|
||||
if (groupStartTime === null) {
|
||||
groupStartTime = processedNestedEntry.timeStart;
|
||||
isFirstLinked = Boolean(processedNestedEntry.linkStart);
|
||||
}
|
||||
|
||||
// lastEntry is the event with the latest end time
|
||||
groupEndTime = processedNestedEntry.timeEnd;
|
||||
if (j > 0) {
|
||||
processedEntry.duration += processedNestedEntry.gap;
|
||||
}
|
||||
processedEntry.duration = processedEntry.duration + processedNestedEntry.duration;
|
||||
if (groupStartTime === null) {
|
||||
groupStartTime = processedNestedEntry.timeStart;
|
||||
isFirstLinked = Boolean(processedNestedEntry.linkStart);
|
||||
}
|
||||
|
||||
// update group metadata
|
||||
processedEntry.timeStart = groupStartTime;
|
||||
processedEntry.timeEnd = groupEndTime;
|
||||
processedEntry.isFirstLinked = isFirstLinked;
|
||||
processedEntry.entries = groupEvents;
|
||||
groupEndTime = processedNestedEntry.timeEnd;
|
||||
if (j > 0) {
|
||||
processedEntry.duration += processedNestedEntry.gap;
|
||||
}
|
||||
processedEntry.duration += processedNestedEntry.duration;
|
||||
}
|
||||
|
||||
processedEntry.timeStart = groupStartTime;
|
||||
processedEntry.timeEnd = groupEndTime;
|
||||
processedEntry.isFirstLinked = isFirstLinked;
|
||||
processedEntry.entries = groupEvents;
|
||||
}
|
||||
|
||||
return getMetadata();
|
||||
|
||||
@@ -205,10 +205,15 @@ export type ProcessedRundownMetadata = RundownMetadata & {
|
||||
|
||||
/**
|
||||
* Factory function to create a rundown metadata processor
|
||||
* @returns {process, getMetadata} process() - processes entries in order | getMetadata() -> returns the current metadata
|
||||
*
|
||||
* @param customFields project custom fields used to sanitise entries
|
||||
* @param options.mutate when true, entries are mutated in place rather than cloned.
|
||||
* Callers must own (or have already cloned) the input rundown.
|
||||
* @returns process() - processes entries in order | getMetadata() returns the accumulated metadata
|
||||
*/
|
||||
export function makeRundownMetadata(customFields: CustomFields) {
|
||||
let rundownMeta: ProcessedRundownMetadata = {
|
||||
export function makeRundownMetadata(customFields: CustomFields, options?: { mutate?: boolean }) {
|
||||
const mutate = options?.mutate ?? false;
|
||||
const rundownMeta: ProcessedRundownMetadata = {
|
||||
totalDelay: 0,
|
||||
totalDuration: 0,
|
||||
totalDays: 0,
|
||||
@@ -228,13 +233,8 @@ export function makeRundownMetadata(customFields: CustomFields) {
|
||||
previousEntry: null,
|
||||
};
|
||||
|
||||
function process<T extends OntimeEntry>(
|
||||
entry: T,
|
||||
childOfGroup: EntryId | null,
|
||||
): { processedData: ProcessedRundownMetadata; processedEntry: T } {
|
||||
const data = processEntry(rundownMeta, customFields, entry, childOfGroup);
|
||||
rundownMeta = data.processedData;
|
||||
return data;
|
||||
function process<T extends OntimeEntry>(entry: T, childOfGroup: EntryId | null): T {
|
||||
return processEntry(rundownMeta, customFields, mutate ? entry : structuredClone(entry), childOfGroup);
|
||||
}
|
||||
|
||||
function getMetadata(): ProcessedRundownMetadata {
|
||||
@@ -245,20 +245,19 @@ export function makeRundownMetadata(customFields: CustomFields) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single entry and updates the rundown metadata
|
||||
* Processes a single entry, mutating both `entry` and `rundownMetadata` in place.
|
||||
* Returns the same `entry` reference for caller convenience.
|
||||
*/
|
||||
function processEntry<T extends OntimeEntry>(
|
||||
rundownMetadata: ProcessedRundownMetadata,
|
||||
customFields: CustomFields,
|
||||
entry: T,
|
||||
childOfGroup: EntryId | null,
|
||||
): { processedData: ProcessedRundownMetadata; processedEntry: T } {
|
||||
const processedData = { ...rundownMetadata };
|
||||
const currentEntry = structuredClone(entry);
|
||||
processedData.flatEntryOrder.push(currentEntry.id);
|
||||
): T {
|
||||
rundownMetadata.flatEntryOrder.push(entry.id);
|
||||
|
||||
if (isOntimeEvent(currentEntry)) {
|
||||
processedData.timedEventOrder.push(currentEntry.id);
|
||||
if (isOntimeEvent(entry)) {
|
||||
rundownMetadata.timedEventOrder.push(entry.id);
|
||||
|
||||
/**
|
||||
* 1.Checks that link can be established (ie, events exist and are valid)
|
||||
@@ -266,91 +265,91 @@ function processEntry<T extends OntimeEntry>(
|
||||
* The linked event is always the previous playable event
|
||||
* If no previous event exists, the link is removed
|
||||
*/
|
||||
if (currentEntry.linkStart) {
|
||||
if (processedData.previousEvent) {
|
||||
const timePatch = getLinkedTimes(currentEntry, processedData.previousEvent);
|
||||
currentEntry.timeStart = timePatch.timeStart;
|
||||
currentEntry.timeEnd = timePatch.timeEnd;
|
||||
currentEntry.duration = timePatch.duration;
|
||||
if (entry.linkStart) {
|
||||
if (rundownMetadata.previousEvent) {
|
||||
const timePatch = getLinkedTimes(entry, rundownMetadata.previousEvent);
|
||||
entry.timeStart = timePatch.timeStart;
|
||||
entry.timeEnd = timePatch.timeEnd;
|
||||
entry.duration = timePatch.duration;
|
||||
} else {
|
||||
currentEntry.linkStart = false;
|
||||
entry.linkStart = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. handle custom fields - mutates currentEntry
|
||||
sanitiseCustomFields(customFields, currentEntry);
|
||||
// 2. handle custom fields - mutates entry
|
||||
sanitiseCustomFields(customFields, entry);
|
||||
|
||||
processedData.totalDays += calculateDayOffset(currentEntry, processedData.previousEvent);
|
||||
currentEntry.dayOffset = processedData.totalDays as Day;
|
||||
currentEntry.delay = 0; // this means we dont calculate delays or gaps for skipped events
|
||||
currentEntry.gap = 0; // this means we dont calculate delays or gaps for skipped events
|
||||
currentEntry.parent = childOfGroup;
|
||||
rundownMetadata.totalDays += calculateDayOffset(entry, rundownMetadata.previousEvent);
|
||||
entry.dayOffset = rundownMetadata.totalDays as Day;
|
||||
entry.delay = 0; // this means we dont calculate delays or gaps for skipped events
|
||||
entry.gap = 0; // this means we dont calculate delays or gaps for skipped events
|
||||
entry.parent = childOfGroup;
|
||||
|
||||
// update rundown metadata, it only concerns playable events
|
||||
if (isPlayableEvent(currentEntry)) {
|
||||
processedData.playableEventOrder.push(currentEntry.id);
|
||||
if (isPlayableEvent(entry)) {
|
||||
rundownMetadata.playableEventOrder.push(entry.id);
|
||||
|
||||
// first start is always the first event
|
||||
if (processedData.firstStart === null) {
|
||||
processedData.firstStart = currentEntry.timeStart;
|
||||
if (rundownMetadata.firstStart === null) {
|
||||
rundownMetadata.firstStart = entry.timeStart;
|
||||
}
|
||||
|
||||
// check if event is flagged
|
||||
if (currentEntry.flag) {
|
||||
processedData.flags.push(currentEntry.id);
|
||||
if (entry.flag) {
|
||||
rundownMetadata.flags.push(entry.id);
|
||||
}
|
||||
|
||||
currentEntry.gap = getTimeFrom(currentEntry, processedData.latestEvent);
|
||||
entry.gap = getTimeFrom(entry, rundownMetadata.latestEvent);
|
||||
|
||||
if (currentEntry.gap === 0) {
|
||||
if (entry.gap === 0) {
|
||||
// event starts on previous finish, we add its duration
|
||||
processedData.totalDuration += currentEntry.duration;
|
||||
} else if (currentEntry.gap > 0) {
|
||||
rundownMetadata.totalDuration += entry.duration;
|
||||
} else if (entry.gap > 0) {
|
||||
// event has a gap, we add the gap and the duration
|
||||
processedData.totalDuration += currentEntry.gap + currentEntry.duration;
|
||||
} else if (currentEntry.gap < 0) {
|
||||
rundownMetadata.totalDuration += entry.gap + entry.duration;
|
||||
} else {
|
||||
// there is an overlap, we remove the overlap from the duration
|
||||
// ensuring that the sum is not negative (ie: fully overlapped events)
|
||||
// NOTE: we add the gap since it is a negative number
|
||||
processedData.totalDuration += Math.max(currentEntry.duration + currentEntry.gap, 0);
|
||||
rundownMetadata.totalDuration += Math.max(entry.duration + entry.gap, 0);
|
||||
}
|
||||
|
||||
// remove eventual gaps from the accumulated delay
|
||||
// we only affect positive delays (time forwards)
|
||||
if (processedData.totalDelay > 0 && currentEntry.gap > 0) {
|
||||
if (rundownMetadata.totalDelay > 0 && entry.gap > 0) {
|
||||
let correctedDelay = 0;
|
||||
// we need to separate the delay that is accumulated from one that may exist after the gap
|
||||
if (isOntimeDelay(processedData.previousEntry)) {
|
||||
correctedDelay = processedData.previousEntry.duration;
|
||||
processedData.totalDelay -= correctedDelay;
|
||||
if (isOntimeDelay(rundownMetadata.previousEntry)) {
|
||||
correctedDelay = rundownMetadata.previousEntry.duration;
|
||||
rundownMetadata.totalDelay -= correctedDelay;
|
||||
}
|
||||
processedData.totalDelay = Math.max(processedData.totalDelay - currentEntry.gap, 0);
|
||||
processedData.totalDelay += correctedDelay;
|
||||
rundownMetadata.totalDelay = Math.max(rundownMetadata.totalDelay - entry.gap, 0);
|
||||
rundownMetadata.totalDelay += correctedDelay;
|
||||
}
|
||||
|
||||
// current event delay is the current accumulated delay
|
||||
currentEntry.delay = processedData.totalDelay;
|
||||
entry.delay = rundownMetadata.totalDelay;
|
||||
|
||||
// assign data for next iteration
|
||||
processedData.previousEvent = currentEntry;
|
||||
rundownMetadata.previousEvent = entry;
|
||||
|
||||
// lastEntry is the event with the latest end time
|
||||
if (isNewLatest(currentEntry, processedData.latestEvent)) {
|
||||
processedData.latestEvent = currentEntry;
|
||||
processedData.lastEnd = currentEntry.timeEnd;
|
||||
if (isNewLatest(entry, rundownMetadata.latestEvent)) {
|
||||
rundownMetadata.latestEvent = entry;
|
||||
rundownMetadata.lastEnd = entry.timeEnd;
|
||||
}
|
||||
}
|
||||
} else if (isOntimeDelay(currentEntry)) {
|
||||
} else if (isOntimeDelay(entry)) {
|
||||
// !!! this must happen after handling the links
|
||||
processedData.totalDelay += currentEntry.duration;
|
||||
currentEntry.parent = childOfGroup;
|
||||
rundownMetadata.totalDelay += entry.duration;
|
||||
entry.parent = childOfGroup;
|
||||
}
|
||||
|
||||
if (!childOfGroup) {
|
||||
processedData.order.push(currentEntry.id);
|
||||
rundownMetadata.order.push(entry.id);
|
||||
}
|
||||
processedData.entries[currentEntry.id] = currentEntry;
|
||||
processedData.previousEntry = currentEntry;
|
||||
rundownMetadata.entries[entry.id] = entry;
|
||||
rundownMetadata.previousEntry = entry;
|
||||
|
||||
return { processedData, processedEntry: currentEntry };
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { ErrorResponse, OntimeEntry, ProjectRundownsList, RenumberCues, Rundown
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { makeNewRundown } from '../../models/dataModel.js';
|
||||
import { paramsWithId } from '../validation-utils/validationFunction.js';
|
||||
import { getCurrentRundown } from './rundown.dao.js';
|
||||
import { getCurrentRundown, getProcessedRundown } from './rundown.dao.js';
|
||||
import {
|
||||
addEntry,
|
||||
applyDelay,
|
||||
batchEditEntries,
|
||||
cloneEntry,
|
||||
createNewRundown,
|
||||
deleteAllEntries,
|
||||
deleteEntries,
|
||||
editEntry,
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
entrySwapValidator,
|
||||
rundownArrayOfIds,
|
||||
rundownPostValidator,
|
||||
validateRundownMutation,
|
||||
} from './rundown.validation.js';
|
||||
|
||||
export const router: Router = express.Router();
|
||||
@@ -58,6 +57,19 @@ router.get('/current', async (_req: Request, res: Response<Rundown>) => {
|
||||
res.json(rundown);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a given rundown in its normalised client shape
|
||||
*/
|
||||
router.get('/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = getProcessedRundown(req.params.id);
|
||||
res.json(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(404).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads a given rundown
|
||||
*/
|
||||
@@ -76,11 +88,7 @@ router.post('/:id/load', paramsWithId, async (req: Request, res: Response<Projec
|
||||
*/
|
||||
router.post('/', rundownPostValidator, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||
try {
|
||||
const emptyRundown = makeNewRundown();
|
||||
emptyRundown.title = req.body.title;
|
||||
await getDataProvider().setRundown(emptyRundown.id, emptyRundown);
|
||||
|
||||
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||
const projectRundowns = await createNewRundown(req.body.title);
|
||||
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -180,10 +188,9 @@ router.delete('/:id', paramsWithId, async (req: Request, res: Response<ProjectRu
|
||||
router.post(
|
||||
'/:rundownId/entry',
|
||||
entryPostValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
|
||||
try {
|
||||
const newEvent = await addEntry(req.body);
|
||||
const newEvent = await addEntry(req.params.rundownId, req.body);
|
||||
res.status(201).send(newEvent);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -195,20 +202,15 @@ router.post(
|
||||
/**
|
||||
* Edits an entry in a given rundown
|
||||
*/
|
||||
router.put(
|
||||
'/:rundownId/entry',
|
||||
entryPutValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
|
||||
try {
|
||||
const event = await editEntry(req.body);
|
||||
res.status(200).send(event);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.put('/:rundownId/entry', entryPutValidator, async (req: Request, res: Response<OntimeEntry | ErrorResponse>) => {
|
||||
try {
|
||||
const event = await editEntry(req.params.rundownId, req.body);
|
||||
res.status(200).send(event);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Edits an entry in a given rundown
|
||||
@@ -216,10 +218,9 @@ router.put(
|
||||
router.put(
|
||||
'/:rundownId/batch',
|
||||
entryBatchPutValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await batchEditEntries(req.body.ids, req.body.data);
|
||||
const rundown = await batchEditEntries(req.params.rundownId, req.body.ids, req.body.data);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -234,11 +235,10 @@ router.put(
|
||||
router.patch(
|
||||
'/:rundownId/reorder',
|
||||
entryReorderValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const { entryId, destinationId, order } = req.body;
|
||||
const rundown = await reorderEntry(entryId, destinationId, order);
|
||||
const rundown = await reorderEntry(req.params.rundownId, entryId, destinationId, order);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -253,10 +253,9 @@ router.patch(
|
||||
router.patch(
|
||||
'/:rundownId/applydelay/:id',
|
||||
paramsWithId,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await applyDelay(req.params.id);
|
||||
const rundown = await applyDelay(req.params.rundownId, req.params.id);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -268,20 +267,15 @@ router.patch(
|
||||
/**
|
||||
* Swaps data between two Ontime events
|
||||
*/
|
||||
router.patch(
|
||||
'/:rundownId/swap',
|
||||
entrySwapValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await swapEvents(req.body.from, req.body.to);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.patch('/:rundownId/swap', entrySwapValidator, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await swapEvents(req.params.rundownId, req.body.from, req.body.to);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clones the contents of an entry into a new one
|
||||
@@ -290,10 +284,9 @@ router.post(
|
||||
'/:rundownId/clone/:id',
|
||||
paramsWithId,
|
||||
clonePostValidator,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await cloneEntry(req.params.id, {
|
||||
const rundown = await cloneEntry(req.params.rundownId, req.params.id, {
|
||||
before: req.body?.before,
|
||||
after: req.body?.after,
|
||||
});
|
||||
@@ -308,38 +301,28 @@ router.post(
|
||||
/**
|
||||
* Creates a group out of a list of entries
|
||||
*/
|
||||
router.post(
|
||||
'/:rundownId/group',
|
||||
rundownArrayOfIds,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await groupEntries(req.body.ids);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post('/:rundownId/group', rundownArrayOfIds, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await groupEntries(req.params.rundownId, req.body.ids);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dissolves a group by moving its children to the main rundown
|
||||
*/
|
||||
router.post(
|
||||
'/:rundownId/ungroup/:id',
|
||||
paramsWithId,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await ungroupEntries(req.params.id);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post('/:rundownId/ungroup/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await ungroupEntries(req.params.rundownId, req.params.id);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a list of entries by their ID
|
||||
@@ -347,10 +330,9 @@ router.post(
|
||||
router.delete(
|
||||
'/:rundownId/entries',
|
||||
rundownArrayOfIds,
|
||||
validateRundownMutation,
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await deleteEntries(req.body.ids);
|
||||
const rundown = await deleteEntries(req.params.rundownId, req.body.ids);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -362,19 +344,15 @@ router.delete(
|
||||
/**
|
||||
* Deletes all entries in a given rundown
|
||||
*/
|
||||
router.delete(
|
||||
'/:rundownId/all',
|
||||
validateRundownMutation,
|
||||
async (_req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await deleteAllEntries();
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete('/:rundownId/all', async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const rundown = await deleteAllEntries(req.params.rundownId);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Reorders two entries in a rundown
|
||||
@@ -382,11 +360,10 @@ router.delete(
|
||||
router.patch(
|
||||
'/:rundownId/renumber',
|
||||
entryRenumberValidator,
|
||||
validateRundownMutation,
|
||||
(req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||
try {
|
||||
const { ids, prefix, start, increment } = matchedData<RenumberCues>(req);
|
||||
const rundown = renumberEntries(ids, prefix, start, increment);
|
||||
const rundown = await renumberEntries(req.params.rundownId, ids, prefix, start, increment);
|
||||
res.status(200).send(rundown);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { customFieldLabelToKey, getInsertAfterId, resolveInsertParent } from 'on
|
||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { makeNewRundown } from '../../models/dataModel.js';
|
||||
import { setLastLoadedRundown } from '../../services/app-state-service/AppStateService.js';
|
||||
import { runtimeService } from '../../services/runtime-service/runtime.service.js';
|
||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
@@ -38,8 +39,8 @@ import { generateEvent, getIntegerAndFraction, hasChanges } from './rundown.util
|
||||
/**
|
||||
* creates a new entry with given data
|
||||
*/
|
||||
export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function addEntry(rundownId: string, eventData: EventPostPayload): Promise<OntimeEntry> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
// we allow the user to provide an ID, but make sure it is unique
|
||||
if (eventData?.id && Object.hasOwn(rundown.entries, eventData.id)) {
|
||||
@@ -66,7 +67,7 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
|
||||
// make mutations to rundown
|
||||
rundownMutation.add(rundown, newEntry, afterId, parent);
|
||||
|
||||
const { rundownMetadata, revision } = commit();
|
||||
const { rundown: responseRundown, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -77,14 +78,14 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
|
||||
notifyChanges(rundown.id, rundownMetadata, revision, { timer: [newEntry.id], external: true });
|
||||
});
|
||||
|
||||
return newEntry;
|
||||
return responseRundown.entries[newEntry.id] ?? newEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a patch to an entry in the rundown
|
||||
*/
|
||||
export async function editEntry(patch: PatchWithId): Promise<OntimeEntry> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function editEntry(rundownId: string, patch: PatchWithId): Promise<OntimeEntry> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
const currentEntry = rundown.entries[patch.id];
|
||||
|
||||
/**
|
||||
@@ -110,7 +111,7 @@ export async function editEntry(patch: PatchWithId): Promise<OntimeEntry> {
|
||||
}
|
||||
|
||||
const { entry, didInvalidate } = rundownMutation.edit(rundown, patch);
|
||||
const { rundownMetadata, revision } = commit(didInvalidate);
|
||||
const { rundown: responseRundown, rundownMetadata, revision } = await commit(didInvalidate);
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -124,14 +125,18 @@ export async function editEntry(patch: PatchWithId): Promise<OntimeEntry> {
|
||||
});
|
||||
});
|
||||
|
||||
return entry;
|
||||
return responseRundown.entries[entry.id] ?? entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a patch to several entries in the rundown
|
||||
*/
|
||||
export async function batchEditEntries(ids: EntryId[], patch: Partial<OntimeEntry>): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function batchEditEntries(
|
||||
rundownId: string,
|
||||
ids: EntryId[],
|
||||
patch: Partial<OntimeEntry>,
|
||||
): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
/**
|
||||
* We can do some validation globally, but mostly we will validate each entry individually
|
||||
@@ -176,7 +181,7 @@ export async function batchEditEntries(ids: EntryId[], patch: Partial<OntimeEntr
|
||||
batchDidInvalidate = true;
|
||||
}
|
||||
}
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit(batchDidInvalidate);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit(batchDidInvalidate);
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -196,8 +201,8 @@ export async function batchEditEntries(ids: EntryId[], patch: Partial<OntimeEntr
|
||||
/**
|
||||
* Deletes a known entry from the current rundown
|
||||
*/
|
||||
export async function deleteEntries(entryIds: EntryId[]): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function deleteEntries(rundownId: string, entryIds: EntryId[]): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
for (let i = 0; i < entryIds.length; i++) {
|
||||
const entry = rundown.entries[entryIds[i]];
|
||||
@@ -207,7 +212,7 @@ export async function deleteEntries(entryIds: EntryId[]): Promise<Rundown> {
|
||||
rundownMutation.remove(rundown, entry);
|
||||
}
|
||||
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -224,12 +229,12 @@ export async function deleteEntries(entryIds: EntryId[]): Promise<Rundown> {
|
||||
/**
|
||||
* Deletes all entries from the current rundown
|
||||
*/
|
||||
export async function deleteAllEntries(): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function deleteAllEntries(rundownId: string): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
rundownMutation.removeAll(rundown);
|
||||
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -248,8 +253,13 @@ export async function deleteAllEntries(): Promise<Rundown> {
|
||||
* Handles moving across root orders (a group order and top level order)
|
||||
* @throws if entryId or destinationId not found
|
||||
*/
|
||||
export async function reorderEntry(entryId: EntryId, destinationId: EntryId, order: 'before' | 'after' | 'insert') {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function reorderEntry(
|
||||
rundownId: string,
|
||||
entryId: EntryId,
|
||||
destinationId: EntryId,
|
||||
order: 'before' | 'after' | 'insert',
|
||||
) {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
// check that both entries exist
|
||||
const eventFrom = rundown.entries[entryId];
|
||||
@@ -261,7 +271,7 @@ export async function reorderEntry(entryId: EntryId, destinationId: EntryId, ord
|
||||
|
||||
rundownMutation.reorder(rundown, eventFrom, eventTo, order);
|
||||
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -278,18 +288,24 @@ export async function reorderEntry(entryId: EntryId, destinationId: EntryId, ord
|
||||
/**
|
||||
* @throws if an id is missing or not an Ontime event
|
||||
*/
|
||||
export function renumberEntries(ids: EntryId[], prefix: string, start: string, increment: string): Rundown {
|
||||
export async function renumberEntries(
|
||||
rundownId: string,
|
||||
ids: EntryId[],
|
||||
prefix: string,
|
||||
start: string,
|
||||
increment: string,
|
||||
): Promise<Rundown> {
|
||||
const startNumber = getIntegerAndFraction(start);
|
||||
const incrementNumber = getIntegerAndFraction(increment);
|
||||
|
||||
// if the prefix doesn't already include a separator or is empty, then insert a separator
|
||||
if (prefix !== '' && !prefix.endsWith('-') && !prefix.endsWith(' ')) prefix += ' ';
|
||||
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true, mutableCustomFields: false });
|
||||
|
||||
rundownMutation.renumber(rundown, ids, prefix, startNumber, incrementNumber);
|
||||
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit(false);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit(false);
|
||||
|
||||
setImmediate(() => {
|
||||
updateRuntimeOnChange(rundownMetadata);
|
||||
@@ -303,8 +319,8 @@ export function renumberEntries(ids: EntryId[], prefix: string, start: string, i
|
||||
* Applies a delay into the rundown effectively changing the schedule
|
||||
* The applied delay is deleted
|
||||
*/
|
||||
export async function applyDelay(delayId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function applyDelay(rundownId: string, delayId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
// check that delay exists
|
||||
const delay = rundown.entries[delayId];
|
||||
@@ -316,7 +332,7 @@ export async function applyDelay(delayId: EntryId): Promise<Rundown> {
|
||||
rundownMutation.applyDelay(rundown, delay);
|
||||
rundownMutation.remove(rundown, delay);
|
||||
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -333,8 +349,8 @@ export async function applyDelay(delayId: EntryId): Promise<Rundown> {
|
||||
/**
|
||||
* Swaps the data between two events in the rundown
|
||||
*/
|
||||
export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function swapEvents(rundownId: string, fromId: EntryId, toId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
const eventFrom = rundown.entries[fromId];
|
||||
const eventTo = rundown.entries[toId];
|
||||
|
||||
@@ -349,7 +365,7 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundow
|
||||
}
|
||||
|
||||
rundownMutation.swap(rundown, eventFrom, eventTo);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -368,8 +384,8 @@ export async function swapEvents(fromId: EntryId, toId: EntryId): Promise<Rundow
|
||||
* Handles cloning children if the entry is a group
|
||||
* @throws if the entry to clone does not exist
|
||||
*/
|
||||
export async function cloneEntry(entryId: EntryId, options: InsertOptions): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function cloneEntry(rundownId: string, entryId: EntryId, options: InsertOptions): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
const originalEntry = rundown.entries[entryId];
|
||||
|
||||
if (!originalEntry) {
|
||||
@@ -377,7 +393,7 @@ export async function cloneEntry(entryId: EntryId, options: InsertOptions): Prom
|
||||
}
|
||||
|
||||
const newEntry = rundownMutation.clone(rundown, originalEntry, options);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -400,11 +416,11 @@ export async function cloneEntry(entryId: EntryId, options: InsertOptions): Prom
|
||||
/**
|
||||
* Groups a list of entries into a new group
|
||||
*/
|
||||
export async function groupEntries(entryIds: EntryId[]): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function groupEntries(rundownId: string, entryIds: EntryId[]): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
rundownMutation.group(rundown, entryIds);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -421,8 +437,8 @@ export async function groupEntries(entryIds: EntryId[]): Promise<Rundown> {
|
||||
/**
|
||||
* Deletes a group and moves all its children to the top level
|
||||
*/
|
||||
export async function ungroupEntries(groupId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ mutableRundown: true, mutableCustomFields: false });
|
||||
export async function ungroupEntries(rundownId: string, groupId: EntryId): Promise<Rundown> {
|
||||
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||
|
||||
const group = rundown.entries[groupId];
|
||||
if (!group || !isOntimeGroup(group)) {
|
||||
@@ -430,7 +446,7 @@ export async function ungroupEntries(groupId: EntryId): Promise<Rundown> {
|
||||
}
|
||||
|
||||
rundownMutation.ungroup(rundown, group);
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = commit();
|
||||
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -465,7 +481,7 @@ export async function createCustomField(customField: CustomField): Promise<Custo
|
||||
customFieldMutation.add(customFields, key, customField);
|
||||
|
||||
// Adding a custom field has no immediate implications on the rundown
|
||||
const { customFields: resultCustomFields } = commit(false);
|
||||
const { customFields: resultCustomFields } = await commit(false);
|
||||
|
||||
setImmediate(() => {
|
||||
sendRefetch(RefetchKey.CustomFields);
|
||||
@@ -523,7 +539,7 @@ export async function editCustomField(
|
||||
}
|
||||
|
||||
// the custom fields have been removed and there is no processing to be done
|
||||
const { rundownMetadata, revision, customFields: resultCustomFields } = commit(false);
|
||||
const { rundownMetadata, revision, customFields: resultCustomFields } = await commit(false);
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -562,7 +578,7 @@ export async function deleteCustomField(key: CustomFieldKey, projectRundowns: Pr
|
||||
customFieldMutation.remove(customFields, key);
|
||||
|
||||
// the custom fields have been removed and there is no processing to be done
|
||||
const { rundownMetadata, revision, customFields: resultCustomFields } = commit(false);
|
||||
const { rundownMetadata, revision, customFields: resultCustomFields } = await commit(false);
|
||||
|
||||
// schedule the side effects
|
||||
setImmediate(() => {
|
||||
@@ -579,7 +595,8 @@ export async function deleteCustomField(key: CustomFieldKey, projectRundowns: Pr
|
||||
*
|
||||
* @private - exported for testing
|
||||
*/
|
||||
export function updateRuntimeOnChange(rundownMetadata: RundownMetadata) {
|
||||
export function updateRuntimeOnChange(rundownMetadata: RundownMetadata | null) {
|
||||
if (!rundownMetadata) return;
|
||||
// we only declare the amount of playable events
|
||||
const numEvents = rundownMetadata.timedEventOrder.length;
|
||||
|
||||
@@ -603,16 +620,15 @@ type NotifyChangesOptions = {
|
||||
*/
|
||||
function notifyChanges(
|
||||
rundownId: string | undefined,
|
||||
rundownMetadata: RundownMetadata,
|
||||
rundownMetadata: RundownMetadata | null,
|
||||
revision: number,
|
||||
options: NotifyChangesOptions,
|
||||
) {
|
||||
// notify timer service of changed event
|
||||
if (options.timer && rundownId && isCurrentRundown(rundownId)) {
|
||||
if (rundownMetadata && options.timer && rundownId && isCurrentRundown(rundownId)) {
|
||||
runtimeService.notifyOfChangedEvents(rundownMetadata);
|
||||
}
|
||||
|
||||
// notify external services of changes
|
||||
if (options.reload) {
|
||||
sendRefetch(RefetchKey.All);
|
||||
} else if (options.external) {
|
||||
@@ -656,8 +672,23 @@ export async function initRundown(
|
||||
|
||||
setImmediate(() => {
|
||||
notifyChanges(rundown.id, rundownMetadata, revision, { timer: true, external: true, reload });
|
||||
sendRefetch(RefetchKey.ProjectRundowns);
|
||||
setLastLoadedRundown(rundown.id).catch((error) => {
|
||||
logger.error(LogOrigin.Server, `Failed to persist last loaded rundown: ${error}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNewRundown(title: string) {
|
||||
const emptyRundown = makeNewRundown();
|
||||
emptyRundown.title = title;
|
||||
await getDataProvider().setRundown(emptyRundown.id, emptyRundown);
|
||||
|
||||
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||
|
||||
setImmediate(() => {
|
||||
sendRefetch(RefetchKey.ProjectRundowns);
|
||||
});
|
||||
|
||||
return projectRundowns;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
import { requestValidationFunction } from '../validation-utils/validationFunction.js';
|
||||
import { getCurrentRundown } from './rundown.dao.js';
|
||||
|
||||
// #region operations on project rundowns =========================
|
||||
|
||||
@@ -11,29 +9,6 @@ export const rundownPostValidator = [body('title').isString().trim().notEmpty(),
|
||||
// #endregion operations on project rundowns ======================
|
||||
// #region operations on rundown entries ==========================
|
||||
|
||||
/**
|
||||
* Middleware prevents mutating a rundown that is not selected
|
||||
* This allows our service to still only handle the current rundown
|
||||
*
|
||||
* This would need to be removed in favour or rundown selection if we would like
|
||||
* to implement the mutation of background rundowns
|
||||
*/
|
||||
export async function validateRundownMutation(req: Request, res: Response, next: NextFunction) {
|
||||
const { rundownId } = req.params;
|
||||
|
||||
try {
|
||||
if (getCurrentRundown().id !== rundownId) {
|
||||
res.status(404).json({ message: 'Cannot mutate not selected rundown' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(404).json({ message: 'Rundown not found' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const entryPostValidator = [
|
||||
body('type').isString().isIn(['event', 'delay', 'group', 'milestone']),
|
||||
body('after').optional().isString(),
|
||||
|
||||
Reference in New Issue
Block a user