mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 19:33:46 +00:00
refactor: implement operations on nested events
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
CustomFields,
|
||||
LogOrigin,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
@@ -17,12 +16,12 @@ import { getCueCandidate } from 'ontime-utils';
|
||||
|
||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { createEvent } from '../../utils/parser.js';
|
||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getInsertionPosition } from './rundownUtils.js';
|
||||
|
||||
type CompleteEntry<T> =
|
||||
T extends Partial<OntimeEvent>
|
||||
@@ -40,10 +39,6 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
||||
eventData: T,
|
||||
afterId?: string,
|
||||
): CompleteEntry<T> {
|
||||
// TODO: could we keep the UI ID to avoid the flash on create?
|
||||
// we discard any UI provided IDs and add our own
|
||||
const id = cache.getUniqueId();
|
||||
|
||||
if (isOntimeEvent(eventData)) {
|
||||
const currentRundown = cache.getCurrentRundown();
|
||||
return createEvent(
|
||||
@@ -52,10 +47,13 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
||||
) as CompleteEntry<T>;
|
||||
}
|
||||
|
||||
const id = eventData.id || cache.getUniqueId();
|
||||
|
||||
if (isOntimeDelay(eventData)) {
|
||||
return { ...delayDef, duration: eventData.duration ?? 0, id } as CompleteEntry<T>;
|
||||
}
|
||||
|
||||
// TODO(v4): allow user to provide a larger patch of the block entry
|
||||
if (isOntimeBlock(eventData)) {
|
||||
return { ...blockDef, title: eventData?.title ?? '', id } as CompleteEntry<T>;
|
||||
}
|
||||
@@ -67,41 +65,46 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
||||
* creates a new event with given data
|
||||
*/
|
||||
export async function addEvent(eventData: EventPostPayload): Promise<OntimeEntry> {
|
||||
// if the user didnt provide an index, we add the event to start
|
||||
let atIndex = 0;
|
||||
let afterId: string | undefined = eventData?.after;
|
||||
// 1. we allow the user to provide an ID, but make sure it is unique
|
||||
if (eventData?.id && cache.hasId(eventData.id)) {
|
||||
throw new Error(`Event with ID ${eventData.id} already exists`);
|
||||
}
|
||||
|
||||
if (afterId) {
|
||||
const previousIndex = cache.getIndexOf(afterId);
|
||||
if (previousIndex < 0) {
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${afterId}`);
|
||||
} else {
|
||||
atIndex = previousIndex + 1;
|
||||
// 2. if the user provides a parent (inside a group), we make sure it exists
|
||||
let parent: EntryId | undefined;
|
||||
if ('parent' in eventData && eventData.parent != null) {
|
||||
if (!cache.hasId(eventData.parent)) {
|
||||
throw new Error(`Parent event with ID ${eventData.parent} not found`);
|
||||
}
|
||||
} else if (eventData?.before !== undefined) {
|
||||
const previousIndex = cache.getIndexOf(eventData.before);
|
||||
if (previousIndex < 0) {
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.before}`);
|
||||
} else {
|
||||
atIndex = previousIndex;
|
||||
if (previousIndex > 0) {
|
||||
afterId = cache.getIdOf(atIndex - 1);
|
||||
}
|
||||
parent = eventData.parent;
|
||||
}
|
||||
|
||||
// 3. if the user provides an after or before ID, we make sure it exists
|
||||
if (eventData?.after !== undefined) {
|
||||
if (!cache.hasId(eventData.after)) {
|
||||
throw new Error(`Event with ID ${eventData.after} not found`);
|
||||
}
|
||||
}
|
||||
if (eventData?.before !== undefined) {
|
||||
if (!cache.hasId(eventData.before)) {
|
||||
throw new Error(`Event with ID ${eventData.before} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// generate a fully formed event from the patch
|
||||
const eventToAdd = generateEvent(eventData, afterId);
|
||||
const { afterId, atIndex } = getInsertionPosition(parent, eventData?.after, eventData?.before);
|
||||
|
||||
// generate a fully formed entry from the patch
|
||||
const sanitisedEntry = generateEvent(eventData, afterId);
|
||||
|
||||
// modify rundown
|
||||
const scopedMutation = cache.mutateCache(cache.add);
|
||||
const { newEvent } = await scopedMutation({ atIndex, event: eventToAdd });
|
||||
const { newEvent } = await scopedMutation({ atIndex, parent, entry: sanitisedEntry });
|
||||
|
||||
// notify runtime that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
|
||||
// notify timer and external services of change
|
||||
notifyChanges({ timer: [eventToAdd.id], external: true });
|
||||
notifyChanges({ timer: [sanitisedEntry.id], external: true });
|
||||
|
||||
// we know this mutation returns an OntimeEntry
|
||||
return newEvent as OntimeEntry;
|
||||
|
||||
@@ -26,6 +26,7 @@ let currentRundown: Rundown = {
|
||||
id: '',
|
||||
title: '',
|
||||
order: [],
|
||||
flatOrder: [],
|
||||
entries: {},
|
||||
revision: 0,
|
||||
};
|
||||
@@ -101,6 +102,9 @@ export function generate(
|
||||
// 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);
|
||||
|
||||
// if the event is a block, we process the nested entries
|
||||
@@ -115,6 +119,10 @@ export function generate(
|
||||
for (let i = 0; i < processedEntry.events.length; i++) {
|
||||
const nestedEntryId = processedEntry.events[i];
|
||||
const nestedEntry = initialRundown.entries[nestedEntryId];
|
||||
|
||||
if (!nestedEntry) {
|
||||
continue;
|
||||
}
|
||||
const { processedData: processedNestedData, processedEntry: processedNestedEntry } = process(
|
||||
nestedEntry,
|
||||
processedEntry.id,
|
||||
@@ -161,14 +169,22 @@ export function updateCache() {
|
||||
|
||||
// update the cache values
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data
|
||||
const { entries, order, previousEvent, latestEvent, ...metadata } = processedData;
|
||||
currentRundown.entries = entries;
|
||||
currentRundown.order = order;
|
||||
const { previousEvent, latestEvent, ...metadata } = processedData;
|
||||
currentRundown.entries = metadata.entries;
|
||||
currentRundown.order = metadata.order;
|
||||
currentRundown.flatOrder = metadata.flatEventOrder;
|
||||
rundownMetadata = metadata;
|
||||
clearIsStale();
|
||||
customFieldChangelog = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a given ID is exists in the current rundown
|
||||
*/
|
||||
export function hasId(id: EntryId): boolean {
|
||||
return Object.hasOwn(currentRundown.entries, id);
|
||||
}
|
||||
|
||||
/** Returns an ID guaranteed to be unique */
|
||||
export function getUniqueId(): string {
|
||||
if (isStale) {
|
||||
@@ -177,19 +193,19 @@ export function getUniqueId(): string {
|
||||
let id = '';
|
||||
do {
|
||||
id = generateId();
|
||||
} while (Object.hasOwn(currentRundown.entries, id));
|
||||
} while (hasId(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Returns index of an event with a given id */
|
||||
export function getIndexOf(eventId: EntryId) {
|
||||
/** Returns index of an entry with a given id */
|
||||
export function getIndexOf(entryId: EntryId) {
|
||||
if (isStale) {
|
||||
updateCache();
|
||||
}
|
||||
return currentRundown.order.indexOf(eventId);
|
||||
return currentRundown.order.indexOf(entryId);
|
||||
}
|
||||
|
||||
/** Returns id of an event at a given index */
|
||||
/** Returns id of an entry at a given index */
|
||||
export function getIdOf(index: number) {
|
||||
if (isStale) {
|
||||
updateCache();
|
||||
@@ -304,34 +320,60 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
return scopedMutation;
|
||||
}
|
||||
|
||||
type AddArgs = MutationParams<{ atIndex: number; event: OntimeEntry }>;
|
||||
type AddArgs = MutationParams<{ atIndex: number; parent?: EntryId; entry: OntimeEntry }>;
|
||||
/**
|
||||
* Add entry to rundown
|
||||
*/
|
||||
export function add({ rundown, atIndex, event }: AddArgs): Required<MutatingReturn> {
|
||||
const newEvent: OntimeEntry = { ...event };
|
||||
export function add({ rundown, atIndex, parent, entry }: AddArgs): Required<MutatingReturn> {
|
||||
const newEntry: OntimeEntry = { ...entry };
|
||||
|
||||
rundown.entries[newEntry.id] = newEntry;
|
||||
|
||||
if (parent) {
|
||||
const parentBlock = rundown.entries[parent] as OntimeBlock;
|
||||
parentBlock.events = insertAtIndex(atIndex, newEntry.id, parentBlock.events);
|
||||
} else {
|
||||
rundown.order = insertAtIndex(atIndex, newEntry.id, rundown.order);
|
||||
}
|
||||
|
||||
rundown.entries[newEvent.id] = newEvent;
|
||||
rundown.order = insertAtIndex(atIndex, newEvent.id, rundown.order);
|
||||
setIsStale();
|
||||
return { newRundown: rundown, newEvent, didMutate: true };
|
||||
return { newRundown: rundown, newEvent: newEntry, didMutate: true };
|
||||
}
|
||||
|
||||
type RemoveArgs = MutationParams<{ eventIds: EntryId[] }>;
|
||||
/**
|
||||
* Remove entry to rundown
|
||||
* Remove entries in a rundown
|
||||
*/
|
||||
export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
||||
const previousLength = rundown.order.length;
|
||||
rundown.order = rundown.order.filter((id) => !eventIds.includes(id));
|
||||
for (const id of eventIds) {
|
||||
delete rundown.entries[id];
|
||||
let didMutate = false;
|
||||
|
||||
for (let i = 0; i < eventIds.length; i++) {
|
||||
const entry = rundown.entries[eventIds[i]];
|
||||
if (isOntimeEvent(entry) && entry.parent) {
|
||||
const parentBlock = rundown.entries[entry.parent] as OntimeBlock;
|
||||
edit({
|
||||
rundown,
|
||||
eventId: entry.parent,
|
||||
patch: {
|
||||
events: parentBlock.events.filter((id) => id !== eventIds[i]),
|
||||
numEvents: parentBlock.events.length - 1,
|
||||
},
|
||||
});
|
||||
parentBlock.events = parentBlock.events.filter((id) => id !== entry.id);
|
||||
} else {
|
||||
rundown.order = rundown.order.filter((id) => id !== eventIds[i]);
|
||||
}
|
||||
didMutate = true;
|
||||
delete rundown.entries[eventIds[i]];
|
||||
}
|
||||
const didMutate = rundown.order.length !== previousLength;
|
||||
|
||||
if (didMutate) setIsStale();
|
||||
return { newRundown: rundown, didMutate };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries of a rundown
|
||||
*/
|
||||
export function removeAll(): MutatingReturn {
|
||||
setIsStale();
|
||||
return {
|
||||
@@ -339,6 +381,7 @@ export function removeAll(): MutatingReturn {
|
||||
id: '',
|
||||
title: '',
|
||||
order: [],
|
||||
flatOrder: [],
|
||||
entries: {},
|
||||
revision: 0,
|
||||
},
|
||||
@@ -441,6 +484,7 @@ export function reorder({ rundown, eventId, from, to }: ReorderArgs): Required<M
|
||||
type ApplyDelayArgs = MutationParams<{ delayId: EntryId }>;
|
||||
/**
|
||||
* Apply a delay
|
||||
* Mutates the given rundown
|
||||
*/
|
||||
export function applyDelay({ rundown, delayId }: ApplyDelayArgs): MutatingReturn {
|
||||
apply(delayId, rundown);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EntryId,
|
||||
RundownEntries,
|
||||
ProjectRundowns,
|
||||
OntimeBlock,
|
||||
} from 'ontime-types';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
@@ -173,3 +174,38 @@ export function getRundownOrThrow(rundowns: ProjectRundowns, rundownId: string):
|
||||
}
|
||||
return rundowns[rundownId];
|
||||
}
|
||||
|
||||
export function getInsertionPosition(
|
||||
parentId?: EntryId,
|
||||
afterId?: EntryId,
|
||||
beforeId?: EntryId,
|
||||
): { atIndex: number; afterId: EntryId | undefined } {
|
||||
if (afterId) {
|
||||
const order = selectOrderList(parentId);
|
||||
return {
|
||||
atIndex: order.findIndex((id) => id === afterId) + 1,
|
||||
afterId,
|
||||
};
|
||||
}
|
||||
|
||||
if (beforeId) {
|
||||
const order = selectOrderList(parentId);
|
||||
const atIndex = order.findIndex((id) => id === beforeId);
|
||||
return {
|
||||
atIndex,
|
||||
afterId: order[atIndex - 1] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
atIndex: 0,
|
||||
afterId: undefined,
|
||||
};
|
||||
|
||||
function selectOrderList(parentId?: EntryId) {
|
||||
if (parentId) {
|
||||
return (getEntryWithId(parentId) as OntimeBlock).events;
|
||||
}
|
||||
return cache.getEventOrder().order;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user