mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
refactor: restructure model to contain an object of rundowns
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { deleteFile } from '../../utils/parserUtils.js';
|
||||
import { parseDatabaseModel } from '../../utils/parser.js';
|
||||
import { parseRundown } from '../../utils/parserFunctions.js';
|
||||
import { parseRundowns } from '../../utils/parserFunctions.js';
|
||||
import { demoDb } from '../../models/demoProject.js';
|
||||
import { config } from '../../setup/config.js';
|
||||
import { getDataProvider, initPersistence } from '../../classes/data-provider/DataProvider.js';
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
moveCorruptFile,
|
||||
parseJsonFile,
|
||||
} from './projectServiceUtils.js';
|
||||
import { getFirstRundown } from '../rundown-service/rundownUtils.js';
|
||||
|
||||
// init dependencies
|
||||
init();
|
||||
@@ -83,7 +84,7 @@ async function loadNewProject(): Promise<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Private function handles side effects on currupted files
|
||||
* Private function handles side effects on corrupted files
|
||||
* Corrupted files in this context contain data that failed domain validation
|
||||
*/
|
||||
async function handleCorruptedFile(filePath: string, fileName: string): Promise<string> {
|
||||
@@ -176,10 +177,11 @@ export async function loadProjectFile(name: string) {
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields } = result.data;
|
||||
const { rundowns, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
const firstRundown = getFirstRundown(rundowns);
|
||||
await initRundown(firstRundown, customFields);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,10 +248,11 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields } = result.data;
|
||||
const { rundowns, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
const firstRundown = getFirstRundown(rundowns);
|
||||
await initRundown(firstRundown, customFields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,17 +303,23 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
||||
runtimeService.stop();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we need to remove the fields before merging
|
||||
const { rundown, customFields, ...rest } = data;
|
||||
const { rundowns, customFields, ...rest } = data;
|
||||
// we can pass some stuff straight to the data provider
|
||||
const newData = await getDataProvider().mergeIntoData(rest);
|
||||
await getDataProvider().mergeIntoData(rest);
|
||||
|
||||
// ... but rundown and custom fields need to be checked
|
||||
if (rundown != null) {
|
||||
const result = parseRundown(data);
|
||||
await initRundown(result.rundown, result.customFields);
|
||||
if (rundowns != null) {
|
||||
const result = parseRundowns(data);
|
||||
/**
|
||||
* The user may have multiple rundowns
|
||||
* We currently ignore all other rundowns
|
||||
*/
|
||||
const firstRundown = getFirstRundown(result.rundowns);
|
||||
initRundown(firstRundown, result.customFields);
|
||||
}
|
||||
|
||||
return newData;
|
||||
const updatedData = await getDataProvider().getData();
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,12 +44,12 @@ describe('duplicateProjectFile', () => {
|
||||
await expect(duplicateProjectFile('does not exist', 'doesnt matter')).rejects.toThrow('Project file not found');
|
||||
});
|
||||
|
||||
it('throws an error if new file name is already a project', () => {
|
||||
it('throws an error if new file name is already a project', async () => {
|
||||
// current project exists
|
||||
(doesProjectExist as Mock).mockReturnValueOnce('thisoneexists');
|
||||
// new project exists
|
||||
(doesProjectExist as Mock).mockReturnValueOnce('existingproject');
|
||||
expect(duplicateProjectFile('thisoneexists', 'existingproject')).rejects.toThrow(
|
||||
await expect(duplicateProjectFile('thisoneexists', 'existingproject')).rejects.toThrow(
|
||||
'Project file with name existingproject already exists',
|
||||
);
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe('renameProjectFile', () => {
|
||||
(doesProjectExist as Mock).mockReturnValueOnce('this one exists');
|
||||
// new project exists
|
||||
(doesProjectExist as Mock).mockReturnValueOnce('existingproject');
|
||||
expect(renameProjectFile('this one exists', 'existingproject')).rejects.toThrow(
|
||||
await expect(renameProjectFile('this one exists', 'existingproject')).rejects.toThrow(
|
||||
'Project file with name existingproject already exists',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,13 +4,14 @@ import {
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundownEntry,
|
||||
OntimeEntry,
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
OntimeRundown,
|
||||
PatchWithId,
|
||||
EventPostPayload,
|
||||
Rundown,
|
||||
EntryId,
|
||||
} from 'ontime-types';
|
||||
import { getCueCandidate } from 'ontime-utils';
|
||||
|
||||
@@ -22,7 +23,6 @@ import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||
|
||||
type CompleteEntry<T> =
|
||||
T extends Partial<OntimeEvent>
|
||||
@@ -33,15 +33,23 @@ type CompleteEntry<T> =
|
||||
? OntimeBlock
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Generates a fully formed RundownEntry of the patch type
|
||||
*/
|
||||
function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>>(
|
||||
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)) {
|
||||
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), afterId)) as CompleteEntry<T>;
|
||||
const currentRundown = cache.getCurrentRundown();
|
||||
return createEvent(
|
||||
eventData,
|
||||
getCueCandidate(currentRundown.entries, currentRundown.order, afterId),
|
||||
) as CompleteEntry<T>;
|
||||
}
|
||||
|
||||
if (isOntimeDelay(eventData)) {
|
||||
@@ -56,19 +64,17 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
||||
}
|
||||
|
||||
/**
|
||||
* @description creates a new event with given data
|
||||
* @param {object} eventData
|
||||
* @return {OntimeRundownEntry}
|
||||
* creates a new event with given data
|
||||
*/
|
||||
export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundownEntry> {
|
||||
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;
|
||||
|
||||
if (eventData?.after !== undefined) {
|
||||
const previousIndex = cache.getIndexOf(eventData.after);
|
||||
if (afterId) {
|
||||
const previousIndex = cache.getIndexOf(afterId);
|
||||
if (previousIndex < 0) {
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${afterId}`);
|
||||
} else {
|
||||
atIndex = previousIndex + 1;
|
||||
}
|
||||
@@ -79,7 +85,7 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundo
|
||||
} else {
|
||||
atIndex = previousIndex;
|
||||
if (previousIndex > 0) {
|
||||
afterId = cache.getPersistedRundown()[atIndex - 1].id;
|
||||
afterId = cache.getIdOf(atIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,14 +101,14 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundo
|
||||
updateRuntimeOnChange();
|
||||
|
||||
// notify timer and external services of change
|
||||
notifyChanges({ timer: [eventData.id], external: true });
|
||||
notifyChanges({ timer: [eventToAdd.id], external: true });
|
||||
|
||||
return newEvent;
|
||||
// we know this mutation returns an OntimeEntry
|
||||
return newEvent as OntimeEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes event by its ID
|
||||
* @param eventId
|
||||
*/
|
||||
export async function deleteEvent(eventIds: string[]) {
|
||||
const scopedMutation = cache.mutateCache(cache.remove);
|
||||
@@ -194,9 +200,9 @@ export async function reorderEvent(eventId: string, from: number, to: number) {
|
||||
return reorderedItem;
|
||||
}
|
||||
|
||||
export async function applyDelay(eventId: string) {
|
||||
export async function applyDelay(delayId: EntryId) {
|
||||
const scopedMutation = cache.mutateCache(cache.applyDelay);
|
||||
await scopedMutation({ eventId });
|
||||
await scopedMutation({ delayId });
|
||||
|
||||
// notify runtime that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
@@ -227,8 +233,8 @@ export async function swapEvents(from: string, to: string) {
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateRuntimeOnChange() {
|
||||
const timedEvents = getTimedEvents();
|
||||
const numEvents = timedEvents.length;
|
||||
const { timedEventsOrder } = cache.getEventOrder();
|
||||
const numEvents = timedEventsOrder.length;
|
||||
const metadata = cache.getMetadata();
|
||||
|
||||
// schedule an update for the end of the event loop
|
||||
@@ -251,9 +257,9 @@ type NotifyChangesOptions = {
|
||||
*/
|
||||
function notifyChanges(options: NotifyChangesOptions) {
|
||||
if (options.timer) {
|
||||
const playableEvents = getPlayableEvents();
|
||||
const { playableEventsOrder } = cache.getEventOrder();
|
||||
|
||||
if (playableEvents.length === 0) {
|
||||
if (playableEventsOrder.length === 0) {
|
||||
runtimeService.stop();
|
||||
} else {
|
||||
// notify timer service of changed events
|
||||
@@ -279,7 +285,7 @@ function notifyChanges(options: NotifyChangesOptions) {
|
||||
* Overrides the rundown with the given
|
||||
* @param rundown
|
||||
*/
|
||||
export async function initRundown(rundown: Readonly<OntimeRundown>, customFields: Readonly<CustomFields>) {
|
||||
export async function initRundown(rundown: Readonly<Rundown>, customFields: Readonly<CustomFields>) {
|
||||
await cache.init(rundown, customFields);
|
||||
|
||||
// notify runtime that rundown has changed
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SupportedEvent, OntimeEvent, OntimeDelay } from 'ontime-types';
|
||||
import { SupportedEvent, OntimeEvent, OntimeDelay, OntimeBlock, Rundown } from 'ontime-types';
|
||||
import { defaultRundown } from '../../../models/dataModel.js';
|
||||
|
||||
const baseEvent = {
|
||||
type: SupportedEvent.Event,
|
||||
@@ -6,6 +7,10 @@ const baseEvent = {
|
||||
revision: 1,
|
||||
};
|
||||
|
||||
const baseBlock = {
|
||||
type: SupportedEvent.Block,
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to create a Ontime event
|
||||
*/
|
||||
@@ -19,8 +24,25 @@ export function makeOntimeEvent(patch: Partial<OntimeEvent>): OntimeEvent {
|
||||
/**
|
||||
* Utility to create a delay event
|
||||
*/
|
||||
export function makeOntimeDelay(duration: number): OntimeDelay {
|
||||
return { id: 'delay', type: SupportedEvent.Delay, duration };
|
||||
export function makeOntimeDelay(patch: Partial<OntimeDelay>): OntimeDelay {
|
||||
return { id: 'delay', type: SupportedEvent.Delay, duration: 0, ...patch } as OntimeDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create a block event
|
||||
*/
|
||||
export function makeOntimeBlock(patch: Partial<OntimeBlock>): OntimeBlock {
|
||||
return { id: 'block', ...baseBlock, ...patch } as OntimeBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create a rundown object
|
||||
*/
|
||||
export function makeRundown(patch: Partial<Rundown>): Rundown {
|
||||
return {
|
||||
...defaultRundown,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,61 +1,84 @@
|
||||
import { OntimeBlock, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { apply } from '../delayUtils.js';
|
||||
import { makeOntimeDelay, makeOntimeEvent } from '../__mocks__/rundown.mocks.js';
|
||||
import { makeOntimeBlock, makeOntimeDelay, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||
|
||||
describe('apply()', () => {
|
||||
it('applies a positive delay to the rundown', () => {
|
||||
const testRundown = [
|
||||
makeOntimeDelay(10),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||
makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
|
||||
makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||
makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
revision: 0,
|
||||
order: ['delay', '1', '2', '3', '4', '5'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 10 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||
'3': makeOntimeBlock({ id: '3' }),
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||
'5': makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).not.toBe(testRundown);
|
||||
expect(updatedRundown).toMatchObject([
|
||||
{ id: '1', timeStart: 10, timeEnd: 20, duration: 10, revision: 2 },
|
||||
{ id: '2', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '1' },
|
||||
{ id: '3' },
|
||||
{ id: '4', timeStart: 30, timeEnd: 40, duration: 10, revision: 2, linkStart: null },
|
||||
{ id: '5', timeStart: 40, timeEnd: 50, duration: 10, revision: 2, linkStart: '4' },
|
||||
]);
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.revision).toBe(1);
|
||||
expect(testRundown.order).toMatchObject(['1', '2', '3', '4', '5']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': { id: '1', timeStart: 10, timeEnd: 20, duration: 10, revision: 2 },
|
||||
'2': { id: '2', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '1' },
|
||||
'3': { id: '3' },
|
||||
'4': { id: '4', timeStart: 30, timeEnd: 40, duration: 10, revision: 2, linkStart: null },
|
||||
'5': { id: '5', timeStart: 40, timeEnd: 50, duration: 10, revision: 2, linkStart: '4' },
|
||||
});
|
||||
});
|
||||
|
||||
it('applies negative delays', () => {
|
||||
const testRundown = [
|
||||
makeOntimeDelay(-10),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||
makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
|
||||
makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||
makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
revision: 0,
|
||||
order: ['delay', '1', '2', '3', '4', '5'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: -10 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 10, duration: 10 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 10, timeEnd: 20, duration: 10, linkStart: '1' }),
|
||||
'3': makeOntimeBlock({ id: '3' }),
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 20, timeEnd: 30, duration: 10, linkStart: null }),
|
||||
'5': makeOntimeEvent({ id: '5', timeStart: 30, timeEnd: 40, duration: 10, linkStart: '4' }),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject([
|
||||
{ id: '1', timeStart: 0, timeEnd: 10, duration: 10, revision: 2 },
|
||||
{ id: '2', timeStart: 0, timeEnd: 10, duration: 10, revision: 2, linkStart: null },
|
||||
{ id: '3' },
|
||||
{ id: '4', timeStart: 10, timeEnd: 20, duration: 10, revision: 2, linkStart: null },
|
||||
{ id: '5', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '4' },
|
||||
]);
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.revision).toBe(1);
|
||||
expect(testRundown.order).toMatchObject(['1', '2', '3', '4', '5']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': { id: '1', timeStart: 0, timeEnd: 10, duration: 10, revision: 2 },
|
||||
'2': { id: '2', timeStart: 0, timeEnd: 10, duration: 10, revision: 2, linkStart: null },
|
||||
'3': { id: '3' },
|
||||
'4': { id: '4', timeStart: 10, timeEnd: 20, duration: 10, revision: 2, linkStart: null },
|
||||
'5': { id: '5', timeStart: 20, timeEnd: 30, duration: 10, revision: 2, linkStart: '4' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should account for minimum duration and start when applying negative delays', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
makeOntimeDelay(-50),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, linkStart: '1' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['delay', '1', '2'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: -50 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
const expected = [
|
||||
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 2 } as OntimeEvent,
|
||||
{
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': {
|
||||
id: '1',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 0,
|
||||
timeEnd: 100,
|
||||
duration: 100,
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 50,
|
||||
@@ -63,173 +86,222 @@ describe('apply()', () => {
|
||||
duration: 50,
|
||||
linkStart: null,
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject(expected);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks events to maintain gaps when applying positive delays', () => {
|
||||
const testRundown = [
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
makeOntimeDelay(50),
|
||||
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['1', 'delay', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 50 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(apply('delay', testRundown)).toMatchObject([
|
||||
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||
{
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': {
|
||||
id: '1',
|
||||
timeStart: 0,
|
||||
timeEnd: 100,
|
||||
duration: 100,
|
||||
revision: 1,
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 150,
|
||||
timeEnd: 200,
|
||||
duration: 50,
|
||||
linkStart: null,
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('maintains links if there is no gap', () => {
|
||||
const testRundown = [
|
||||
makeOntimeDelay(50),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['delay', '1', '2'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 50 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(apply('delay', testRundown)).toMatchObject([
|
||||
{ id: '1', type: SupportedEvent.Event, timeStart: 50, timeEnd: 150, duration: 100, revision: 2 } as OntimeEvent,
|
||||
{
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': {
|
||||
id: '1',
|
||||
timeStart: 50,
|
||||
timeEnd: 150,
|
||||
duration: 100,
|
||||
revision: 2,
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 150,
|
||||
timeEnd: 200,
|
||||
duration: 50,
|
||||
linkStart: '1',
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks events to maintain gaps when applying negative delays', () => {
|
||||
const testRundown = [
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
makeOntimeDelay(-50),
|
||||
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['1', 'delay', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: -50 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(apply('delay', testRundown)).toMatchObject([
|
||||
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||
{
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': { id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 },
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 50,
|
||||
timeEnd: 100,
|
||||
duration: 50,
|
||||
linkStart: null,
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('gaps reduce positive delay', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
makeOntimeDelay(100),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
// gap 50
|
||||
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50, gap: 50 }),
|
||||
// gap 0
|
||||
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50, gap: 0 }),
|
||||
// gap 50
|
||||
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50, gap: 50 }),
|
||||
// linked
|
||||
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['delay', '1', '2', '3', '4', '5'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 100 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
// gap 50
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50, gap: 50 }),
|
||||
// gap 0
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50, gap: 0 }),
|
||||
// gap 50
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50, gap: 50 }),
|
||||
// linked
|
||||
'5': makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject([
|
||||
{ id: '1', timeStart: 0 + 100, timeEnd: 100 + 100, duration: 100, revision: 2 },
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2', '3', '4', '5']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': { id: '1', timeStart: 0 + 100, timeEnd: 100 + 100, duration: 100, revision: 2 },
|
||||
// gap 50 (100 - 50)
|
||||
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
||||
'2': { id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
||||
// gap 50 (50 - 50)
|
||||
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2, gap: 0 },
|
||||
'3': { id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2, gap: 0 },
|
||||
// gap (delay is 0)
|
||||
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
||||
'4': { id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
||||
// linked
|
||||
{ id: '5', timeStart: 350, timeEnd: 400, duration: 50, revision: 1, linkStart: '4' },
|
||||
]);
|
||||
'5': { id: '5', timeStart: 350, timeEnd: 400, duration: 50, revision: 1, linkStart: '4' },
|
||||
});
|
||||
});
|
||||
|
||||
it('gaps reduce positive delay (2)', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
makeOntimeDelay(2 * MILLIS_PER_HOUR),
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
timeStart: 46800000, // 13:00:00
|
||||
timeEnd: 50400000, // 14:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
// gap 1h
|
||||
makeOntimeEvent({
|
||||
id: '2',
|
||||
gap: 1 * MILLIS_PER_HOUR,
|
||||
dayOffset: 0,
|
||||
timeStart: 54000000, // 15:00:00
|
||||
timeEnd: 57600000, // 16:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['delay', '1', '2'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 2 * MILLIS_PER_HOUR }),
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
timeStart: 46800000, // 13:00:00
|
||||
timeEnd: 50400000, // 14:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
// gap 1h
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
gap: 1 * MILLIS_PER_HOUR,
|
||||
dayOffset: 0,
|
||||
timeStart: 54000000, // 15:00:00
|
||||
timeEnd: 57600000, // 16:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject([
|
||||
{ id: '1', timeStart: 54000000 /* 16 */, revision: 2 },
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', '2']);
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': { id: '1', timeStart: 54000000 /* 16 */, revision: 2 },
|
||||
// gap 1h (2h - 1h)
|
||||
{ id: '2', timeStart: 57600000 /* 16 */, revision: 2 },
|
||||
]);
|
||||
'2': { id: '2', timeStart: 57600000 /* 16 */, revision: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it('removes empty delays without applying changes', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
makeOntimeDelay(0),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['delay', '1'],
|
||||
entries: {
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 0 }),
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject([{ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }]);
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1']);
|
||||
expect(testRundown.entries).toMatchObject({ '1': { id: '1', timeStart: 0, timeEnd: 100, duration: 100 } });
|
||||
});
|
||||
|
||||
it('removes delays in last position without applying changes', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
makeOntimeDelay(100),
|
||||
];
|
||||
const testRundown = makeRundown({
|
||||
order: ['1', 'delay'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRundown = apply('delay', testRundown);
|
||||
expect(updatedRundown).toMatchObject([{ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }]);
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1']);
|
||||
expect(testRundown.entries).toMatchObject({ '1': { id: '1', timeStart: 0, timeEnd: 100, duration: 100 } });
|
||||
});
|
||||
|
||||
it('unlinks events to across blocks is it is the first event after the delay', () => {
|
||||
const testRundown = [
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
makeOntimeDelay(50),
|
||||
{ id: 'block', type: SupportedEvent.Block } as OntimeBlock,
|
||||
makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
];
|
||||
expect(apply('delay', testRundown)).toMatchObject([
|
||||
{ id: '1', type: SupportedEvent.Event, timeStart: 0, timeEnd: 100, duration: 100, revision: 1 } as OntimeEvent,
|
||||
{ id: 'block', type: SupportedEvent.Block },
|
||||
{
|
||||
const testRundown = makeRundown({
|
||||
order: ['1', 'delay', 'block', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100, revision: 1 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 50 }),
|
||||
block: makeOntimeBlock({ id: 'block' }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 150, duration: 50, revision: 1, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
apply('delay', testRundown);
|
||||
expect(testRundown.order).toMatchObject(['1', 'block', '2']);
|
||||
|
||||
expect(testRundown.entries).toMatchObject({
|
||||
'1': {
|
||||
id: '1',
|
||||
timeStart: 0,
|
||||
timeEnd: 100,
|
||||
duration: 100,
|
||||
revision: 1,
|
||||
},
|
||||
block: { id: 'block' },
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEvent.Event,
|
||||
timeStart: 150,
|
||||
timeEnd: 200,
|
||||
duration: 50,
|
||||
linkStart: null,
|
||||
revision: 2,
|
||||
} as OntimeEvent,
|
||||
]);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
CustomFields,
|
||||
EventCustomFields,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
} from 'ontime-types';
|
||||
import { CustomFields, OntimeEvent, SupportedEvent, TimeStrategy } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
|
||||
|
||||
import {
|
||||
@@ -23,6 +14,7 @@ import {
|
||||
removeCustomField,
|
||||
customFieldChangelog,
|
||||
} from '../rundownCache.js';
|
||||
import { makeOntimeBlock, makeOntimeDelay, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
||||
@@ -39,13 +31,16 @@ beforeAll(() => {
|
||||
|
||||
describe('generate()', () => {
|
||||
it('creates normalised versions of a given rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1' }),
|
||||
'2': makeOntimeBlock({ id: '2' }),
|
||||
'3': makeOntimeDelay({ id: '3' }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(3);
|
||||
expect(initResult.order).toStrictEqual(['1', '2', '3']);
|
||||
expect(initResult.rundown['1'].type).toBe(SupportedEvent.Event);
|
||||
@@ -54,29 +49,35 @@ describe('generate()', () => {
|
||||
});
|
||||
|
||||
it('calculates delays versions of a given rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Delay, id: '1', duration: 100 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 1, timeEnd: 100 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeDelay({ id: '1', duration: 100 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 1, timeEnd: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(2);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(100);
|
||||
expect(initResult.totalDelay).toBe(100);
|
||||
});
|
||||
|
||||
it('accounts for gaps in rundown when calculating delays', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Delay, id: 'delay', duration: 200 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', 'delay', '2', 'block', '3', 'another-block', '4'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: 200 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
|
||||
block: makeOntimeBlock({ id: 'block', title: 'break' }),
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 400, timeEnd: 500, duration: 100 }),
|
||||
'another-block': makeOntimeBlock({ id: 'another-block', title: 'another-break' }),
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 600, timeEnd: 700, duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(7);
|
||||
expect((initResult.rundown['1'] as OntimeEvent).delay).toBe(0);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(200);
|
||||
@@ -87,76 +88,84 @@ describe('generate()', () => {
|
||||
});
|
||||
|
||||
it('accounts for overlaps in rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 }),
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start
|
||||
});
|
||||
|
||||
it('accounts for overlaps in rundown (with added gap)', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 15000, timeEnd: 20000, duration: 5000 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3', '4'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 }),
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 }),
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 15000, timeEnd: 20000, duration: 5000 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start
|
||||
});
|
||||
|
||||
it('accounts for overlaps in rundown (with multiple days)', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: 9 * MILLIS_PER_HOUR + 15 * MILLIS_PER_MINUTE,
|
||||
timeEnd: 9 * MILLIS_PER_HOUR + 45 * MILLIS_PER_MINUTE,
|
||||
duration: 30 * MILLIS_PER_MINUTE,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '3',
|
||||
timeStart: 9 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '4',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3', '4'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 9 * MILLIS_PER_HOUR + 15 * MILLIS_PER_MINUTE,
|
||||
timeEnd: 9 * MILLIS_PER_HOUR + 45 * MILLIS_PER_MINUTE,
|
||||
duration: 30 * MILLIS_PER_MINUTE,
|
||||
}),
|
||||
'3': makeOntimeEvent({
|
||||
id: '3',
|
||||
timeStart: 9 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
'4': makeOntimeEvent({
|
||||
id: '4',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
duration: MILLIS_PER_HOUR,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start
|
||||
});
|
||||
|
||||
it('handles negative delays', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Delay, id: 'delay', duration: -200 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', 'delay', '2', 'block', '3', 'another-block', '4'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
|
||||
delay: makeOntimeDelay({ id: 'delay', duration: -200 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
|
||||
block: makeOntimeBlock({ id: 'block', title: 'break' }),
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 400, timeEnd: 500, duration: 100 }),
|
||||
'another-block': makeOntimeBlock({ id: 'another-block', title: 'another-break' }),
|
||||
'4': makeOntimeEvent({ id: '4', timeStart: 600, timeEnd: 700, duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(7);
|
||||
expect((initResult.rundown['1'] as OntimeEvent).delay).toBe(0);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(-200);
|
||||
@@ -167,38 +176,38 @@ describe('generate()', () => {
|
||||
});
|
||||
|
||||
it('links times across events', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: 1,
|
||||
duration: 1,
|
||||
timeEnd: 2,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: 11,
|
||||
duration: 1,
|
||||
timeEnd: 12,
|
||||
linkStart: '1',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Delay, id: 'delay' } as OntimeDelay,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '3',
|
||||
timeStart: 21,
|
||||
duration: 1,
|
||||
timeEnd: 22,
|
||||
linkStart: '2',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', 'block', 'delay', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 1,
|
||||
duration: 1,
|
||||
timeEnd: 2,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 11,
|
||||
duration: 1,
|
||||
timeEnd: 12,
|
||||
linkStart: '1',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
}),
|
||||
block: makeOntimeBlock({ id: 'block' }),
|
||||
delay: makeOntimeDelay({ id: 'delay' }),
|
||||
'3': makeOntimeEvent({
|
||||
id: '3',
|
||||
timeStart: 21,
|
||||
duration: 1,
|
||||
timeEnd: 22,
|
||||
linkStart: '2',
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(5);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).timeStart).toBe(2);
|
||||
expect((initResult.rundown['2'] as OntimeEvent).timeEnd).toBe(12);
|
||||
@@ -213,13 +222,16 @@ describe('generate()', () => {
|
||||
});
|
||||
|
||||
it('links times across events, reordered', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 1, timeEnd: 2 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 21, timeEnd: 22, linkStart: '2' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 11, timeEnd: 12, linkStart: '1' } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '3', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 1, timeEnd: 2 }),
|
||||
'3': makeOntimeEvent({ id: '3', timeStart: 21, timeEnd: 22, linkStart: '2' }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 11, timeEnd: 12, linkStart: '1' }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(3);
|
||||
expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(2);
|
||||
expect(initResult.links['1']).toBe('3');
|
||||
@@ -227,159 +239,156 @@ describe('generate()', () => {
|
||||
});
|
||||
|
||||
it('calculates total duration', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'skipped',
|
||||
skip: true,
|
||||
timeStart: 300,
|
||||
timeEnd: 400,
|
||||
duration: 100,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', 'skipped', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
|
||||
skipped: makeOntimeEvent({ id: 'skipped', skip: true, timeStart: 300, timeEnd: 400, duration: 100 }),
|
||||
'3': makeOntimeEvent({ id: '2', timeStart: 400, timeEnd: 500, duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(4);
|
||||
expect(initResult.totalDuration).toBe(500 - 100);
|
||||
});
|
||||
|
||||
it('calculates total duration with 0 duration events without causing a next day', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300, duration: 200 } as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'skipped',
|
||||
skip: true,
|
||||
timeStart: 300,
|
||||
timeEnd: 400,
|
||||
duration: 0,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', 'skipped', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 100, duration: 0 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 300, duration: 200 }),
|
||||
skipped: makeOntimeEvent({ id: 'skipped', skip: true, timeStart: 300, timeEnd: 400, duration: 0 }),
|
||||
'3': makeOntimeEvent({ id: '2', timeStart: 400, timeEnd: 500, duration: 100 }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(4);
|
||||
expect(initResult.totalDuration).toBe(500 - 100);
|
||||
});
|
||||
|
||||
it('calculates total duration across days with gap', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '3',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
}),
|
||||
'3': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
||||
});
|
||||
|
||||
it('calculates total duration across days', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: 12 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
duration: 10 * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 8 * MILLIS_PER_HOUR,
|
||||
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 12 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
duration: 10 * MILLIS_PER_HOUR,
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 8 * MILLIS_PER_HOUR,
|
||||
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
||||
});
|
||||
|
||||
it('handles updating event sequence', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '97cc3e',
|
||||
timeStart: 0,
|
||||
timeEnd: 600000,
|
||||
duration: 600000,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
linkStart: null,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'e01948',
|
||||
timeStart: 600000,
|
||||
timeEnd: 601000,
|
||||
duration: 85801000, // <------------- value out of sync
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: '97cc3e',
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '25c1af',
|
||||
timeStart: 100, // <------------- value out of sync
|
||||
timeEnd: 602000,
|
||||
duration: 0,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: 'e01948',
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 0,
|
||||
timeEnd: 600000,
|
||||
duration: 600000,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
linkStart: null,
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
timeStart: 600000,
|
||||
timeEnd: 601000,
|
||||
duration: 85801000, // <------------- value out of sync
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: '1',
|
||||
}),
|
||||
'3': makeOntimeEvent({
|
||||
id: '3',
|
||||
timeStart: 100, // <------------- value out of sync
|
||||
timeEnd: 602000,
|
||||
duration: 0,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
linkStart: '2',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.rundown).toMatchObject({
|
||||
'97cc3e': {
|
||||
'1': {
|
||||
timeStart: 0,
|
||||
timeEnd: 600000,
|
||||
duration: 600000,
|
||||
timeStrategy: 'lock-duration',
|
||||
linkStart: null,
|
||||
},
|
||||
e01948: {
|
||||
'2': {
|
||||
timeStart: 600000,
|
||||
timeEnd: 601000,
|
||||
duration: 1000,
|
||||
timeStrategy: 'lock-end',
|
||||
linkStart: '97cc3e',
|
||||
linkStart: '1',
|
||||
},
|
||||
'25c1af': {
|
||||
'3': {
|
||||
timeStart: 601000,
|
||||
timeEnd: 602000,
|
||||
duration: 1000,
|
||||
timeStrategy: 'lock-end',
|
||||
linkStart: 'e01948',
|
||||
linkStart: '2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes links if invalid', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 1, linkStart: '10' } as OntimeEvent,
|
||||
];
|
||||
const initResult = generate(testRundown);
|
||||
const rundown = makeRundown({
|
||||
order: ['1'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', timeStart: 1, linkStart: '10' }),
|
||||
},
|
||||
});
|
||||
|
||||
const initResult = generate(rundown);
|
||||
expect(initResult.order.length).toBe(1);
|
||||
expect((initResult.rundown['1'] as OntimeEvent).timeStart).toBe(1);
|
||||
expect(Object.keys(initResult.links).length).toBe(0);
|
||||
@@ -399,24 +408,26 @@ describe('generate()', () => {
|
||||
colour: 'red',
|
||||
},
|
||||
};
|
||||
const testRundown: OntimeRundown = [
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
custom: {
|
||||
lighting: 'event 1 lx',
|
||||
} as EventCustomFields,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
custom: {
|
||||
lighting: 'event 2 lx',
|
||||
sound: 'event 2 sound',
|
||||
} as EventCustomFields,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
const initResult = generate(testRundown, customProperties);
|
||||
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({
|
||||
id: '1',
|
||||
custom: {
|
||||
lighting: 'event 1 lx',
|
||||
},
|
||||
}),
|
||||
'2': makeOntimeEvent({
|
||||
id: '2',
|
||||
custom: {
|
||||
lighting: 'event 2 lx',
|
||||
sound: 'event 2 sound',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const initResult = generate(rundown, customProperties);
|
||||
expect(initResult.order.length).toBe(2);
|
||||
expect(initResult.assignedCustomFields).toMatchObject({
|
||||
lighting: ['1', '2'],
|
||||
@@ -433,47 +444,64 @@ describe('generate()', () => {
|
||||
|
||||
describe('add() mutation', () => {
|
||||
test('adds an event to the rundown', () => {
|
||||
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||
const testRundown: OntimeRundown = [];
|
||||
const { newRundown } = add({ atIndex: 0, event: mockEvent, rundown: testRundown });
|
||||
expect(newRundown.length).toBe(1);
|
||||
expect(newRundown[0]).toMatchObject(mockEvent);
|
||||
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||
const rundown = makeRundown({});
|
||||
const { newRundown } = add({ atIndex: 0, event: mockEvent, rundown });
|
||||
expect(newRundown.order.length).toBe(1);
|
||||
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove() mutation', () => {
|
||||
test('deletes an event from the rundown', () => {
|
||||
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||
const testRundown: OntimeRundown = [mockEvent];
|
||||
const { newRundown } = remove({ eventIds: [mockEvent.id], rundown: testRundown });
|
||||
expect(newRundown.length).toBe(0);
|
||||
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||
const rundown = makeRundown({
|
||||
order: ['mock'],
|
||||
entries: {
|
||||
mock: mockEvent,
|
||||
},
|
||||
});
|
||||
|
||||
const { newRundown } = remove({ eventIds: [mockEvent.id], rundown });
|
||||
expect(newRundown.order.length).toBe(0);
|
||||
});
|
||||
|
||||
test('deletes multiple events from the rundown', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '4' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '5' } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '6' } as OntimeEvent,
|
||||
];
|
||||
const { newRundown } = remove({ eventIds: ['1', '2', '3'], rundown: testRundown });
|
||||
expect(newRundown.length).toBe(3);
|
||||
expect(newRundown.at(0)?.id).toBe('4');
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3', '4', '5', '6'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1' }),
|
||||
'2': makeOntimeBlock({ id: '2' }),
|
||||
'3': makeOntimeDelay({ id: '3' }),
|
||||
'4': makeOntimeEvent({ id: '4' }),
|
||||
'5': makeOntimeEvent({ id: '5' }),
|
||||
'6': makeOntimeEvent({ id: '6' }),
|
||||
},
|
||||
});
|
||||
|
||||
const { newRundown } = remove({ eventIds: ['1', '2', '3'], rundown });
|
||||
expect(newRundown.order.length).toBe(3);
|
||||
expect(newRundown.entries[newRundown.order[0]].id).toBe('4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit() mutation', () => {
|
||||
test('edits an event in the rundown', () => {
|
||||
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||
const mockEventPatch = { cue: 'patched' } as OntimeEvent;
|
||||
const testRundown: OntimeRundown = [mockEvent];
|
||||
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||
const mockEventPatch = makeOntimeEvent({ cue: 'patched' });
|
||||
const rundown = makeRundown({
|
||||
order: ['mock'],
|
||||
entries: {
|
||||
mock: mockEvent,
|
||||
},
|
||||
});
|
||||
|
||||
const { newRundown, newEvent } = edit({
|
||||
eventId: mockEvent.id,
|
||||
patch: mockEventPatch,
|
||||
rundown: testRundown,
|
||||
rundown,
|
||||
});
|
||||
expect(newRundown.length).toBe(1);
|
||||
expect(newRundown.order.length).toBe(1);
|
||||
expect(newEvent).toMatchObject({
|
||||
id: 'mock',
|
||||
cue: 'patched',
|
||||
@@ -484,73 +512,96 @@ describe('edit() mutation', () => {
|
||||
|
||||
describe('batchEdit() mutation', () => {
|
||||
it('should correctly apply the patch to the events with the given IDs', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent,
|
||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent,
|
||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent,
|
||||
];
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', cue: 'data1' }),
|
||||
'2': makeOntimeEvent({ id: '2', cue: 'data2' }),
|
||||
'3': makeOntimeEvent({ id: '3', cue: 'data3' }),
|
||||
},
|
||||
});
|
||||
|
||||
const eventIds = ['1', '3'];
|
||||
const patch = { cue: 'newData' };
|
||||
|
||||
const { newRundown } = batchEdit({ rundown: testRundown, eventIds, patch });
|
||||
const { newRundown } = batchEdit({ rundown, eventIds, patch });
|
||||
|
||||
expect(newRundown).toMatchObject([
|
||||
{ id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2' },
|
||||
{ id: '3', type: SupportedEvent.Event, cue: 'newData' },
|
||||
]);
|
||||
expect(newRundown.entries).toMatchObject({
|
||||
'1': { id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
||||
'2': { id: '2', type: SupportedEvent.Event, cue: 'data2' },
|
||||
'3': { id: '3', type: SupportedEvent.Event, cue: 'newData' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorder() mutation', () => {
|
||||
it('should correctly reorder two events', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent,
|
||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent,
|
||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent,
|
||||
];
|
||||
const { newRundown } = reorder({
|
||||
rundown: testRundown,
|
||||
eventId: testRundown[0].id,
|
||||
from: 0,
|
||||
to: testRundown.length - 1,
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', cue: 'data1', revision: 0 }),
|
||||
'2': makeOntimeEvent({ id: '2', cue: 'data2', revision: 0 }),
|
||||
'3': makeOntimeEvent({ id: '3', cue: 'data3', revision: 0 }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(newRundown).toMatchObject([
|
||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 1 },
|
||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 1 },
|
||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 1 },
|
||||
]);
|
||||
// move first event to the end
|
||||
const { newRundown } = reorder({
|
||||
rundown: rundown,
|
||||
eventId: rundown.order[0],
|
||||
from: 0,
|
||||
to: rundown.order.length - 1,
|
||||
});
|
||||
|
||||
expect(newRundown.order).toStrictEqual(['2', '3', '1']);
|
||||
expect(newRundown.entries).toMatchObject({
|
||||
'2': { id: '2', cue: 'data2', revision: 1 },
|
||||
'3': { id: '3', cue: 'data3', revision: 1 },
|
||||
'1': { id: '1', cue: 'data1', revision: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('swap() mutation', () => {
|
||||
it('should correctly swap data between events', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent,
|
||||
{ id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent,
|
||||
{ id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent,
|
||||
];
|
||||
const { newRundown } = swap({
|
||||
rundown: testRundown,
|
||||
fromId: testRundown[0].id,
|
||||
toId: testRundown[1].id,
|
||||
const rundown = makeRundown({
|
||||
order: ['1', '2', '3'],
|
||||
entries: {
|
||||
'1': makeOntimeEvent({ id: '1', cue: 'data1', timeStart: 1, revision: 4 }),
|
||||
'2': makeOntimeEvent({ id: '2', cue: 'data2', timeStart: 2, revision: 8 }),
|
||||
'3': makeOntimeEvent({ id: '3', cue: 'data3', timeStart: 3, revision: 12 }),
|
||||
},
|
||||
});
|
||||
|
||||
expect((newRundown[0] as OntimeEvent).id).toBe('1');
|
||||
expect((newRundown[0] as OntimeEvent).cue).toBe('data2');
|
||||
expect((newRundown[0] as OntimeEvent).timeStart).toBe(1);
|
||||
expect((newRundown[0] as OntimeEvent).revision).toBe(1);
|
||||
// swap first and second event
|
||||
const { newRundown } = swap({
|
||||
rundown: rundown,
|
||||
fromId: rundown.order[0],
|
||||
toId: rundown.order[1],
|
||||
});
|
||||
|
||||
expect((newRundown[1] as OntimeEvent).id).toBe('2');
|
||||
expect((newRundown[1] as OntimeEvent).cue).toBe('data1');
|
||||
expect((newRundown[1] as OntimeEvent).timeStart).toBe(2);
|
||||
expect((newRundown[1] as OntimeEvent).revision).toBe(1);
|
||||
expect(newRundown.order).toStrictEqual(['1', '2', '3']);
|
||||
|
||||
expect((newRundown[2] as OntimeEvent).id).toBe('3');
|
||||
expect((newRundown[2] as OntimeEvent).cue).toBe('data3');
|
||||
expect((newRundown[2] as OntimeEvent).timeStart).toBe(3);
|
||||
expect((newRundown[2] as OntimeEvent).revision).toBe(0);
|
||||
expect(newRundown.entries['1']).toMatchObject({
|
||||
id: '1',
|
||||
cue: 'data2',
|
||||
timeStart: 1,
|
||||
revision: 5,
|
||||
});
|
||||
|
||||
expect(newRundown.entries['2']).toMatchObject({
|
||||
id: '2',
|
||||
cue: 'data1',
|
||||
timeStart: 2,
|
||||
revision: 9,
|
||||
});
|
||||
|
||||
expect(newRundown.entries['3']).toMatchObject({
|
||||
id: '3',
|
||||
cue: 'data3',
|
||||
timeStart: 3,
|
||||
revision: 12,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
CustomFields,
|
||||
EndAction,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
RundownEntries,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
@@ -10,46 +10,25 @@ import {
|
||||
import {
|
||||
addToCustomAssignment,
|
||||
calculateDayOffset,
|
||||
getLink,
|
||||
handleCustomField,
|
||||
handleLink,
|
||||
hasChanges,
|
||||
isDataStale,
|
||||
} from '../rundownCacheUtils.js';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
describe('getLink()', () => {
|
||||
it('should return null if there is no link', () => {
|
||||
const rundown = [
|
||||
{ type: SupportedEvent.Block, id: 'block' },
|
||||
{ type: SupportedEvent.Event, id: '1' },
|
||||
] as OntimeRundown;
|
||||
|
||||
const result = getLink(1, rundown);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns previous event', () => {
|
||||
const rundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeEnd: 100 },
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 0, linkStart: '1' },
|
||||
] as OntimeRundown;
|
||||
|
||||
const result = getLink(1, rundown);
|
||||
expect(result.id).toBe('1');
|
||||
});
|
||||
});
|
||||
import { makeOntimeBlock, makeOntimeEvent } from '../__mocks__/rundown.mocks.js';
|
||||
|
||||
describe('handleLink()', () => {
|
||||
it('populates data in object and updates link map', () => {
|
||||
const rundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeEnd: 100 },
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 0, linkStart: '1' },
|
||||
] as OntimeRundown;
|
||||
const mutableEvent = { ...rundown[1] } as OntimeEvent;
|
||||
const entries: RundownEntries = {
|
||||
'1': makeOntimeEvent({ id: '1', timeEnd: 100 }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 0, linkStart: '1' }),
|
||||
};
|
||||
|
||||
const mutableEvent = { ...entries[2] } as OntimeEvent;
|
||||
const links = {};
|
||||
|
||||
const result = handleLink(1, rundown, mutableEvent, links);
|
||||
const result = handleLink(mutableEvent, entries[1] as OntimeEvent, links);
|
||||
expect(result).toBeUndefined();
|
||||
expect(mutableEvent.timeStart).toBe(100);
|
||||
expect(mutableEvent.linkStart).toBe('1');
|
||||
@@ -57,17 +36,17 @@ describe('handleLink()', () => {
|
||||
});
|
||||
|
||||
it('removes link if linked event is not found', () => {
|
||||
const rundown = [
|
||||
{ type: SupportedEvent.Block, id: '1' },
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 0, linkStart: '1' },
|
||||
] as OntimeRundown;
|
||||
const mutableEvent = { ...rundown[1] } as OntimeEvent;
|
||||
const entries: RundownEntries = {
|
||||
'1': makeOntimeBlock({ id: '1' }),
|
||||
'2': makeOntimeEvent({ id: '2', timeStart: 0, linkStart: '1' }),
|
||||
};
|
||||
const mutableEvent = { ...entries[2] } as OntimeEvent;
|
||||
const links = {};
|
||||
|
||||
const result = handleLink(1, rundown, mutableEvent, links);
|
||||
const result = handleLink(mutableEvent, null, links);
|
||||
expect(result).toBeUndefined();
|
||||
expect(mutableEvent.timeStart).toBe(0);
|
||||
expect(mutableEvent.linkStart).toBe(null);
|
||||
expect(mutableEvent.linkStart).toBe('true');
|
||||
expect(links).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -252,7 +231,7 @@ describe('hasChanges()', () => {
|
||||
|
||||
describe('calculateDayOffset', () => {
|
||||
it('returns 0 if there is no previous event', () => {
|
||||
expect(calculateDayOffset({ timeStart: 0 })).toBe(0);
|
||||
expect(calculateDayOffset({ timeStart: 0 }, null)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 if the previous event duration is 0', () => {
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
import { OntimeRundown, isOntimeDelay, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { Rundown, EntryId, isOntimeDelay, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { deleteAtIndex } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Applies delay from given event ID, deletes the delay event after
|
||||
* @throws {Error} if event ID not found or is not a delay
|
||||
* Mutates the given rundown
|
||||
* @throws if event ID not found or is not a delay
|
||||
*/
|
||||
export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||
const delayEvent = rundown.at(delayIndex);
|
||||
export function apply(delayId: EntryId, rundown: Rundown): Rundown {
|
||||
const delayEvent = rundown.entries[delayId];
|
||||
|
||||
if (!delayEvent) {
|
||||
throw new Error('Given event ID not found');
|
||||
if (!delayEvent || !isOntimeDelay(delayEvent)) {
|
||||
throw new Error('Given delay ID not found');
|
||||
}
|
||||
|
||||
if (!isOntimeDelay(delayEvent)) {
|
||||
throw new Error('Given event ID is not a delay');
|
||||
}
|
||||
const delayIndex = rundown.order.findIndex((entryId) => entryId === delayId);
|
||||
|
||||
// if the delay is empty, or the last element, we can just delete it
|
||||
if (delayEvent.duration === 0 || delayIndex === rundown.length - 1) {
|
||||
return deleteAtIndex(delayIndex, rundown);
|
||||
// if the delay is empty, or the last element
|
||||
// we can just delete it with no further operations
|
||||
if (delayEvent.duration === 0 || delayIndex === rundown.order.length - 1) {
|
||||
delete rundown.entries[delayId];
|
||||
rundown.order = deleteAtIndex(delayIndex, rundown.order);
|
||||
return rundown;
|
||||
}
|
||||
|
||||
/**
|
||||
* We apply the delay to the rundown
|
||||
* This logic is mostly in sync with rundownCache.generate
|
||||
* The difference is that here it will become part of the schedule,
|
||||
* so we cant leave the work for the generate function
|
||||
*/
|
||||
const updatedRundown = structuredClone(rundown);
|
||||
let delayValue = delayEvent.duration;
|
||||
let lastEntry: OntimeEvent | null = null;
|
||||
let isFirstEvent = true;
|
||||
|
||||
for (let i = delayIndex + 1; i < updatedRundown.length; i++) {
|
||||
const currentEntry = updatedRundown[i];
|
||||
for (let i = delayIndex + 1; i < rundown.order.length; i++) {
|
||||
const currentId = rundown.order[i];
|
||||
const currentEntry = rundown.entries[currentId];
|
||||
|
||||
// we don't do operation on other event types
|
||||
if (!isOntimeEvent(currentEntry)) {
|
||||
@@ -77,5 +80,9 @@ export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||
currentEntry.revision += 1;
|
||||
}
|
||||
|
||||
return deleteAtIndex(delayIndex, updatedRundown);
|
||||
delete rundown.entries[delayId];
|
||||
rundown.order = deleteAtIndex(delayIndex, rundown.order);
|
||||
rundown.revision += 1;
|
||||
|
||||
return rundown;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@ import {
|
||||
CustomField,
|
||||
CustomFieldLabel,
|
||||
CustomFields,
|
||||
EntryId,
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeNumber,
|
||||
OntimeBlock,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
OntimeRundownEntry,
|
||||
OntimeEntry,
|
||||
PlayableEvent,
|
||||
Rundown,
|
||||
RundownEntries,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
generateId,
|
||||
@@ -21,26 +24,32 @@ import {
|
||||
isNewLatest,
|
||||
customFieldLabelToKey,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { createPatch } from '../../utils/parser.js';
|
||||
|
||||
import { apply } from './delayUtils.js';
|
||||
import { calculateDayOffset, handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||
|
||||
type EventID = string;
|
||||
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
||||
|
||||
let persistedRundown: OntimeRundown = [];
|
||||
let currentRundownId: EntryId = '';
|
||||
let currentRundown: Rundown = {
|
||||
id: '',
|
||||
title: '',
|
||||
order: [],
|
||||
entries: {},
|
||||
revision: 0,
|
||||
};
|
||||
let persistedCustomFields: CustomFields = {};
|
||||
|
||||
/**
|
||||
* Get the cached rundown without triggering regeneration
|
||||
*/
|
||||
export const getPersistedRundown = (): OntimeRundown => persistedRundown;
|
||||
export const getCurrentRundown = (): Rundown => currentRundown;
|
||||
export const getCustomFields = (): CustomFields => persistedCustomFields;
|
||||
|
||||
let normalisedRundown: NormalisedRundown = {};
|
||||
let order: EventID[] = [];
|
||||
let revision = 0;
|
||||
let playableEventsOrder: EntryId[] = [];
|
||||
let timedEventsOrder: EntryId[] = [];
|
||||
let flatIndexOrder: EntryId[] = [];
|
||||
|
||||
/**
|
||||
* all mutating functions will set this value if there is a need for re-generation
|
||||
@@ -59,7 +68,7 @@ let totalDays = 0;
|
||||
let firstStart: MaybeNumber = null;
|
||||
let lastEnd: MaybeNumber = null;
|
||||
|
||||
let links: Record<EventID, EventID> = {};
|
||||
let links: Record<EntryId, EntryId> = {};
|
||||
|
||||
/**
|
||||
* Object that contains reference of renamed custom fields
|
||||
@@ -76,13 +85,17 @@ export const customFieldChangelog = new Map<string, string>();
|
||||
* Keep track of which custom fields are used.
|
||||
* This will be handy for when we delete custom fields
|
||||
*/
|
||||
let assignedCustomFields: Record<CustomFieldLabel, EventID[]> = {};
|
||||
let assignedCustomFields: Record<CustomFieldLabel, EntryId[]> = {};
|
||||
|
||||
export async function init(initialRundown: Readonly<OntimeRundown>, customFields: Readonly<CustomFields>) {
|
||||
persistedRundown = structuredClone(initialRundown) as OntimeRundown;
|
||||
/**
|
||||
* Receives a rundown which will be processed and used as the new current rundown
|
||||
*/
|
||||
export async function init(initialRundown: Rundown, customFields: Readonly<CustomFields>) {
|
||||
currentRundown = structuredClone(initialRundown);
|
||||
currentRundownId = initialRundown.id;
|
||||
persistedCustomFields = structuredClone(customFields);
|
||||
generate();
|
||||
await getDataProvider().setRundown(persistedRundown);
|
||||
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
||||
await getDataProvider().setCustomFields(customFields);
|
||||
}
|
||||
|
||||
@@ -90,10 +103,7 @@ export async function init(initialRundown: Readonly<OntimeRundown>, customFields
|
||||
* Utility generate cache
|
||||
* @private should not be called outside of `rundownCache.ts`
|
||||
*/
|
||||
export function generate(
|
||||
initialRundown: OntimeRundown = persistedRundown,
|
||||
customFields: CustomFields = persistedCustomFields,
|
||||
) {
|
||||
export function generate(initialRundown: Rundown = currentRundown, customFields: CustomFields = persistedCustomFields) {
|
||||
function clearIsStale() {
|
||||
isStale = false;
|
||||
}
|
||||
@@ -102,8 +112,10 @@ export function generate(
|
||||
// instead of maintaining logic to update it
|
||||
|
||||
assignedCustomFields = {};
|
||||
normalisedRundown = {};
|
||||
order = [];
|
||||
playableEventsOrder = [];
|
||||
timedEventsOrder = [];
|
||||
flatIndexOrder = [];
|
||||
|
||||
links = {};
|
||||
firstStart = null;
|
||||
lastEnd = null;
|
||||
@@ -111,20 +123,30 @@ export function generate(
|
||||
totalDays = 0;
|
||||
totalDelay = 0;
|
||||
|
||||
// temporary parsed rundown
|
||||
const parsedEntries: RundownEntries = {};
|
||||
const parsedOrder: EntryId[] = [];
|
||||
|
||||
/** A playableEvent from the previous iteration */
|
||||
let previousEntry: PlayableEvent | null = null;
|
||||
/** The playableEvent most forwards in time processed so far */
|
||||
let lastEntry: PlayableEvent | null = null;
|
||||
|
||||
for (let i = 0; i < initialRundown.length; i++) {
|
||||
for (let i = 0; i < initialRundown.order.length; i++) {
|
||||
// we assign a reference to the current entry, this will be mutated in place
|
||||
const currentEntry = initialRundown[i];
|
||||
const currentEntryId = initialRundown.order[i];
|
||||
const currentEntry = initialRundown.entries[currentEntryId];
|
||||
flatIndexOrder.push(currentEntryId);
|
||||
|
||||
if (isOntimeEvent(currentEntry)) {
|
||||
currentEntry.delay = 0;
|
||||
currentEntry.gap = 0;
|
||||
timedEventsOrder.push(currentEntryId);
|
||||
|
||||
// 1. handle links - mutates updatedEvent
|
||||
handleLink(i, initialRundown, currentEntry, links);
|
||||
// 1. handle links - mutates currentEntry and links
|
||||
handleLink(currentEntry, previousEntry, links);
|
||||
|
||||
// 2. handle custom fields - mutates updatedEvent
|
||||
// 2. handle custom fields - mutates currentEntry
|
||||
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
||||
|
||||
totalDays += calculateDayOffset(currentEntry, lastEntry);
|
||||
@@ -132,6 +154,7 @@ export function generate(
|
||||
|
||||
// update rundown metadata, it only concerns playable events
|
||||
if (isPlayableEvent(currentEntry)) {
|
||||
playableEventsOrder.push(currentEntryId);
|
||||
// fist start is always the first event
|
||||
if (firstStart === null) {
|
||||
firstStart = currentEntry.timeStart;
|
||||
@@ -160,6 +183,7 @@ export function generate(
|
||||
// current event delay is the current accumulated delay
|
||||
currentEntry.delay = totalDelay;
|
||||
|
||||
previousEntry = currentEntry;
|
||||
// lastEntry is the event with the latest end time
|
||||
if (isNewLatest(currentEntry, lastEntry)) {
|
||||
lastEntry = currentEntry;
|
||||
@@ -178,17 +202,21 @@ export function generate(
|
||||
}
|
||||
|
||||
// add id to order
|
||||
order.push(currentEntry.id);
|
||||
parsedOrder.push(currentEntry.id);
|
||||
// add entry to rundown
|
||||
normalisedRundown[currentEntry.id] = currentEntry;
|
||||
parsedEntries[currentEntry.id] = currentEntry;
|
||||
}
|
||||
|
||||
lastEnd = lastEntry?.timeEnd ?? null;
|
||||
clearIsStale();
|
||||
customFieldChangelog.clear();
|
||||
|
||||
//The return value is used for testing
|
||||
return { rundown: normalisedRundown, order, links, totalDelay, totalDuration, assignedCustomFields };
|
||||
// update the cache values
|
||||
currentRundown.entries = parsedEntries;
|
||||
currentRundown.order = parsedOrder;
|
||||
|
||||
// The return value is used for testing
|
||||
return { rundown: parsedEntries, order: parsedOrder, links, totalDelay, totalDuration, assignedCustomFields };
|
||||
}
|
||||
|
||||
/** Returns an ID guaranteed to be unique */
|
||||
@@ -199,21 +227,31 @@ export function getUniqueId(): string {
|
||||
let id = '';
|
||||
do {
|
||||
id = generateId();
|
||||
} while (Object.hasOwn(normalisedRundown, id));
|
||||
} while (Object.hasOwn(currentRundown.entries, id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Returns index of an event with a given id */
|
||||
export function getIndexOf(eventId: string) {
|
||||
export function getIndexOf(eventId: EntryId) {
|
||||
if (isStale) {
|
||||
generate();
|
||||
}
|
||||
return order.indexOf(eventId);
|
||||
return currentRundown.order.indexOf(eventId);
|
||||
}
|
||||
|
||||
/** Returns id of an event at a given index */
|
||||
export function getIdOf(index: number) {
|
||||
if (isStale) {
|
||||
generate();
|
||||
}
|
||||
return currentRundown.order.at(index);
|
||||
}
|
||||
|
||||
type RundownCache = {
|
||||
rundown: NormalisedRundown;
|
||||
order: string[];
|
||||
id: string;
|
||||
title: string;
|
||||
order: EntryId[];
|
||||
entries: RundownEntries;
|
||||
revision: number;
|
||||
totalDelay: number;
|
||||
totalDuration: number;
|
||||
@@ -228,19 +266,29 @@ export function get(): Readonly<RundownCache> {
|
||||
generate();
|
||||
}
|
||||
return {
|
||||
rundown: normalisedRundown,
|
||||
order,
|
||||
revision,
|
||||
id: currentRundown.id,
|
||||
title: currentRundown.title,
|
||||
entries: currentRundown.entries,
|
||||
order: currentRundown.order,
|
||||
revision: currentRundown.revision,
|
||||
totalDelay,
|
||||
totalDuration,
|
||||
};
|
||||
}
|
||||
|
||||
export type RundownMetadata = {
|
||||
firstStart: MaybeNumber;
|
||||
lastEnd: MaybeNumber;
|
||||
totalDelay: number;
|
||||
totalDuration: number;
|
||||
revision: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns calculated metadata from rundown
|
||||
* Will triggering regeneration if data is stale.
|
||||
*/
|
||||
export function getMetadata() {
|
||||
export function getMetadata(): Readonly<RundownMetadata> {
|
||||
if (isStale) {
|
||||
generate();
|
||||
}
|
||||
@@ -250,15 +298,35 @@ export function getMetadata() {
|
||||
lastEnd,
|
||||
totalDelay,
|
||||
totalDuration,
|
||||
revision,
|
||||
revision: currentRundown.revision,
|
||||
};
|
||||
}
|
||||
|
||||
type CommonParams = { rundown: OntimeRundown };
|
||||
export type RundownOrder = {
|
||||
order: EntryId[];
|
||||
timedEventsOrder: EntryId[];
|
||||
playableEventsOrder: EntryId[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exposes the order of events
|
||||
*/
|
||||
export function getEventOrder(): Readonly<RundownOrder> {
|
||||
if (isStale) {
|
||||
generate();
|
||||
}
|
||||
return {
|
||||
order: currentRundown.order,
|
||||
timedEventsOrder,
|
||||
playableEventsOrder,
|
||||
};
|
||||
}
|
||||
|
||||
type CommonParams = { rundown: Rundown };
|
||||
type MutationParams<T> = T & CommonParams;
|
||||
type MutatingReturn = {
|
||||
newRundown: OntimeRundown;
|
||||
newEvent?: OntimeRundownEntry;
|
||||
newRundown: Rundown;
|
||||
newEvent?: OntimeEntry;
|
||||
didMutate: boolean;
|
||||
};
|
||||
type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingReturn;
|
||||
@@ -269,15 +337,17 @@ type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingRetur
|
||||
*/
|
||||
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
function scopedMutation(params: T) {
|
||||
const { newEvent, newRundown, didMutate } = mutation({ ...params, rundown: persistedRundown });
|
||||
// we work on a copy of the rundown
|
||||
const rundownCopy = structuredClone(currentRundown);
|
||||
const { newEvent, newRundown, didMutate } = mutation({ ...params, rundown: rundownCopy });
|
||||
|
||||
// early return without calling side effects
|
||||
if (!didMutate) {
|
||||
return { newEvent, newRundown, didMutate };
|
||||
}
|
||||
|
||||
revision = revision + 1;
|
||||
persistedRundown = newRundown;
|
||||
newRundown.revision += 1;
|
||||
currentRundown = newRundown;
|
||||
|
||||
// schedule a non priority cache update
|
||||
setImmediate(() => {
|
||||
@@ -286,7 +356,7 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
|
||||
// defer writing to the database
|
||||
setImmediate(async () => {
|
||||
await getDataProvider().setRundown(persistedRundown);
|
||||
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
||||
});
|
||||
|
||||
return { newEvent, newRundown, didMutate };
|
||||
@@ -295,70 +365,91 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
return scopedMutation;
|
||||
}
|
||||
|
||||
type AddArgs = MutationParams<{ atIndex: number; event: OntimeRundownEntry }>;
|
||||
type AddArgs = MutationParams<{ atIndex: number; event: OntimeEntry }>;
|
||||
/**
|
||||
* Add entry to rundown
|
||||
*/
|
||||
export function add({ rundown, atIndex, event }: AddArgs): Required<MutatingReturn> {
|
||||
const newEvent: OntimeRundownEntry = { ...event };
|
||||
const newRundown = insertAtIndex(atIndex, newEvent, rundown);
|
||||
const newEvent: OntimeEntry = { ...event };
|
||||
|
||||
rundown.entries[newEvent.id] = newEvent;
|
||||
rundown.order = insertAtIndex(atIndex, newEvent.id, rundown.order);
|
||||
setIsStale();
|
||||
return { newRundown, newEvent, didMutate: true };
|
||||
return { newRundown: rundown, newEvent, didMutate: true };
|
||||
}
|
||||
|
||||
type RemoveArgs = MutationParams<{ eventIds: string[] }>;
|
||||
type RemoveArgs = MutationParams<{ eventIds: EntryId[] }>;
|
||||
/**
|
||||
* Remove entry to rundown
|
||||
*/
|
||||
export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
||||
const newRundown = rundown.filter((event) => !eventIds.includes(event.id));
|
||||
const didMutate = rundown.length !== newRundown.length;
|
||||
const previousLength = rundown.order.length;
|
||||
rundown.order = rundown.order.filter((id) => !eventIds.includes(id));
|
||||
for (const id of eventIds) {
|
||||
delete rundown.entries[id];
|
||||
}
|
||||
const didMutate = rundown.order.length !== previousLength;
|
||||
if (didMutate) setIsStale();
|
||||
return { newRundown, didMutate };
|
||||
return { newRundown: rundown, didMutate };
|
||||
}
|
||||
|
||||
export function removeAll(): MutatingReturn {
|
||||
setIsStale();
|
||||
return { newRundown: [], didMutate: true };
|
||||
return {
|
||||
newRundown: {
|
||||
id: '',
|
||||
title: '',
|
||||
order: [],
|
||||
entries: {},
|
||||
revision: 0,
|
||||
},
|
||||
didMutate: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for patching an existing event with new data
|
||||
*/
|
||||
function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial<OntimeRundownEntry>): OntimeRundownEntry {
|
||||
function makeEvent<T extends OntimeEntry>(eventFromRundown: T, patch: Partial<T>): T {
|
||||
if (isOntimeEvent(eventFromRundown)) {
|
||||
const newEvent = createPatch(eventFromRundown, patch as OntimeEvent);
|
||||
const newEvent = createPatch(eventFromRundown, patch as Partial<OntimeEvent>);
|
||||
newEvent.revision++;
|
||||
return newEvent;
|
||||
return newEvent as T;
|
||||
}
|
||||
// TODO: exhaustive check
|
||||
return { ...eventFromRundown, ...patch } as OntimeRundownEntry;
|
||||
if (isOntimeBlock(eventFromRundown)) {
|
||||
const newEvent: OntimeBlock = { ...eventFromRundown, ...patch };
|
||||
newEvent.revision++;
|
||||
return newEvent as T;
|
||||
}
|
||||
|
||||
return { ...eventFromRundown, ...patch } as T;
|
||||
}
|
||||
|
||||
type EditArgs = MutationParams<{ eventId: string; patch: Partial<OntimeRundownEntry> }>;
|
||||
type EditArgs = MutationParams<{ eventId: EntryId; patch: Partial<OntimeEntry> }>;
|
||||
/**
|
||||
* Apply patch to an entry with given id
|
||||
*/
|
||||
export function edit({ rundown, eventId, patch }: EditArgs): Required<MutatingReturn> {
|
||||
const indexAt = rundown.findIndex((event) => event.id === eventId);
|
||||
if (indexAt < 0) {
|
||||
throw new Error('Event not found');
|
||||
const entry = rundown.entries[eventId];
|
||||
if (!entry) {
|
||||
// there should be no reason for the entry not to be found
|
||||
// check if it exists in the rundown order
|
||||
rundown.order = rundown.order.filter((id) => id !== eventId);
|
||||
throw new Error('Entry not found');
|
||||
}
|
||||
|
||||
if (patch?.type && rundown[indexAt].type !== patch.type) {
|
||||
// we cannot allow patching to a different type
|
||||
if (patch?.type && entry.type !== patch.type) {
|
||||
throw new Error('Invalid event type');
|
||||
}
|
||||
|
||||
const eventInMemory = rundown[indexAt];
|
||||
|
||||
if (!hasChanges(eventInMemory, patch)) {
|
||||
return { newRundown: rundown, newEvent: eventInMemory, didMutate: false };
|
||||
// if nothing changed, nothing to do
|
||||
if (!hasChanges(entry, patch)) {
|
||||
return { newRundown: rundown, newEvent: entry, didMutate: false };
|
||||
}
|
||||
|
||||
const newEvent = makeEvent(eventInMemory, patch);
|
||||
|
||||
const newRundown = [...rundown];
|
||||
newRundown[indexAt] = newEvent;
|
||||
const newEvent = makeEvent(entry, patch);
|
||||
rundown.entries[newEvent.id] = newEvent;
|
||||
|
||||
// check whether the data warrants recalculation of cache
|
||||
const makeStale = isDataStale(patch);
|
||||
@@ -366,91 +457,77 @@ export function edit({ rundown, eventId, patch }: EditArgs): Required<MutatingRe
|
||||
if (makeStale) {
|
||||
setIsStale();
|
||||
} else {
|
||||
normalisedRundown[newEvent.id] = newEvent;
|
||||
rundown.entries[newEvent.id] = newEvent;
|
||||
}
|
||||
|
||||
return { newRundown, newEvent, didMutate: true };
|
||||
return { newRundown: rundown, newEvent, didMutate: true };
|
||||
}
|
||||
|
||||
type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>;
|
||||
type BatchEditArgs = MutationParams<{ eventIds: EntryId[]; patch: Partial<OntimeEntry> }>;
|
||||
/**
|
||||
* Apply patch to multiple entries
|
||||
*/
|
||||
export function batchEdit({ rundown, eventIds, patch }: BatchEditArgs): MutatingReturn {
|
||||
const ids = new Set(eventIds);
|
||||
|
||||
const newRundown = [];
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
if (ids.has(rundown[i].id)) {
|
||||
if (patch?.type && rundown[i].type !== patch.type) {
|
||||
continue;
|
||||
}
|
||||
const newEvent = makeEvent(rundown[i], patch);
|
||||
newRundown.push(newEvent);
|
||||
} else {
|
||||
newRundown.push(rundown[i]);
|
||||
}
|
||||
for (const eventId of eventIds) {
|
||||
edit({ rundown, eventId, patch });
|
||||
}
|
||||
setIsStale();
|
||||
return { newRundown, didMutate: true };
|
||||
return { newRundown: rundown, didMutate: true };
|
||||
}
|
||||
|
||||
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>;
|
||||
type ReorderArgs = MutationParams<{ eventId: EntryId; from: number; to: number }>;
|
||||
/**
|
||||
* Redorder two entries
|
||||
* Reorder two entries
|
||||
*/
|
||||
export function reorder({ rundown, eventId, from, to }: ReorderArgs): Required<MutatingReturn> {
|
||||
const event = rundown[from];
|
||||
if (!event || eventId !== event.id) {
|
||||
const eventFrom = rundown.entries[eventId];
|
||||
if (!eventFrom) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
const newRundown = reorderArray(rundown, from, to);
|
||||
rundown.order = reorderArray(rundown.order, from, to);
|
||||
|
||||
// increment revision of all events in between
|
||||
for (let i = from; i <= to; i++) {
|
||||
const event = newRundown.at(i);
|
||||
if (isOntimeEvent(event)) {
|
||||
event.revision += 1;
|
||||
const eventId = rundown.order[i];
|
||||
const entry = rundown.entries[eventId];
|
||||
if (isOntimeEvent(entry) || isOntimeBlock(entry)) {
|
||||
entry.revision += 1;
|
||||
}
|
||||
}
|
||||
|
||||
setIsStale();
|
||||
return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true };
|
||||
return { newRundown: rundown, newEvent: eventFrom, didMutate: true };
|
||||
}
|
||||
|
||||
type ApplyDelayArgs = MutationParams<{ eventId: string }>;
|
||||
type ApplyDelayArgs = MutationParams<{ delayId: EntryId }>;
|
||||
/**
|
||||
* Apply a delay
|
||||
*/
|
||||
export function applyDelay({ rundown, eventId }: ApplyDelayArgs): MutatingReturn {
|
||||
const newRundown = apply(eventId, rundown);
|
||||
export function applyDelay({ rundown, delayId }: ApplyDelayArgs): MutatingReturn {
|
||||
apply(delayId, rundown);
|
||||
setIsStale();
|
||||
return { newRundown, didMutate: true };
|
||||
return { newRundown: rundown, didMutate: true };
|
||||
}
|
||||
|
||||
type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
|
||||
type SwapArgs = MutationParams<{ fromId: EntryId; toId: EntryId }>;
|
||||
/**
|
||||
* Swap two entries
|
||||
*/
|
||||
export function swap({ rundown, fromId, toId }: SwapArgs): MutatingReturn {
|
||||
const indexA = rundown.findIndex((event) => event.id === fromId);
|
||||
const eventA = rundown.at(indexA);
|
||||
const fromEvent = rundown.entries[fromId];
|
||||
const toEvent = rundown.entries[toId];
|
||||
|
||||
const indexB = rundown.findIndex((event) => event.id === toId);
|
||||
const eventB = rundown.at(indexB);
|
||||
|
||||
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||
if (!isOntimeEvent(fromEvent) || !isOntimeEvent(toEvent)) {
|
||||
throw new Error('Swap only available for OntimeEvents');
|
||||
}
|
||||
|
||||
const { newA, newB } = swapEventData(eventA, eventB);
|
||||
const newRundown = [...rundown];
|
||||
const [newFrom, newTo] = swapEventData(fromEvent, toEvent);
|
||||
|
||||
newRundown[indexA] = newA;
|
||||
(newRundown[indexA] as OntimeEvent).revision += 1;
|
||||
newRundown[indexB] = newB;
|
||||
(newRundown[indexB] as OntimeEvent).revision += 1;
|
||||
rundown.entries[fromId] = newFrom;
|
||||
rundown.entries[toId] = newTo;
|
||||
|
||||
setIsStale();
|
||||
return { newRundown, didMutate: true };
|
||||
return { newRundown: rundown, didMutate: true };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -468,7 +545,7 @@ function invalidateIfUsed(label: CustomFieldLabel) {
|
||||
// schedule a non priority cache update
|
||||
setImmediate(async () => {
|
||||
generate();
|
||||
await getDataProvider().setRundown(persistedRundown);
|
||||
await getDataProvider().setRundown(currentRundownId, currentRundown);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +1,35 @@
|
||||
import {
|
||||
OntimeEvent,
|
||||
isOntimeEvent,
|
||||
OntimeRundown,
|
||||
CustomFieldLabel,
|
||||
CustomFields,
|
||||
OntimeRundownEntry,
|
||||
OntimeBaseEvent,
|
||||
} from 'ontime-types';
|
||||
import { OntimeEvent, CustomFieldLabel, CustomFields, OntimeEntry, OntimeBaseEvent } from 'ontime-types';
|
||||
import { dayInMs, getLinkedTimes } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Get linked event
|
||||
*/
|
||||
export function getLink(currentIndex: number, rundown: OntimeRundown): OntimeEvent | null {
|
||||
// currently the link is the previous event
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event) && !event.skip) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates data from link, if necessary
|
||||
* Mutates in place mutableEvent
|
||||
* Mutates in place links
|
||||
* Checks that link can be established (ie, events exist and are valid)
|
||||
* and populates the time data from link
|
||||
* With the current implementation, the links is always the previous playable event
|
||||
* Mutates mutableEvent in place
|
||||
* Mutates links in place
|
||||
*/
|
||||
export function handleLink(
|
||||
currentIndex: number,
|
||||
rundown: OntimeRundown,
|
||||
mutableEvent: OntimeEvent,
|
||||
previousEvent: OntimeEvent | null,
|
||||
links: Record<string, string>,
|
||||
): void {
|
||||
if (!mutableEvent.linkStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkedEvent = getLink(currentIndex, rundown);
|
||||
if (!linkedEvent) {
|
||||
mutableEvent.linkStart = null;
|
||||
/**
|
||||
* If no previous event exist, we dont remove the link
|
||||
* this means that the event will keep the behaviour in case a new event is added before
|
||||
* However, we do add its ID to the links and prevent out-of-sync data
|
||||
*/
|
||||
if (!previousEvent) {
|
||||
mutableEvent.linkStart = 'true';
|
||||
return;
|
||||
}
|
||||
|
||||
// sometimes the client cannot set the previous event
|
||||
if (mutableEvent.linkStart === 'true') {
|
||||
mutableEvent.linkStart = linkedEvent.id;
|
||||
}
|
||||
|
||||
links[linkedEvent.id] = mutableEvent.id;
|
||||
|
||||
const timePatch = getLinkedTimes(mutableEvent, linkedEvent);
|
||||
const timePatch = getLinkedTimes(mutableEvent, previousEvent);
|
||||
mutableEvent.linkStart = previousEvent.id;
|
||||
links[previousEvent.id] = mutableEvent.id;
|
||||
// use object.assign to force mutation
|
||||
Object.assign(mutableEvent, timePatch);
|
||||
}
|
||||
@@ -125,7 +103,7 @@ enum RegenerateWhitelist {
|
||||
* given a patch, returns whether all keys are whitelisted
|
||||
* @param path
|
||||
*/
|
||||
export function isDataStale(patch: Partial<OntimeRundownEntry>): boolean {
|
||||
export function isDataStale(patch: Partial<OntimeEntry>): boolean {
|
||||
return Object.keys(patch).some((key) => !(key in RegenerateWhitelist));
|
||||
}
|
||||
|
||||
@@ -157,7 +135,7 @@ export function hasChanges<T extends OntimeBaseEvent>(existingEvent: T, newEvent
|
||||
*/
|
||||
export function calculateDayOffset(
|
||||
current: Pick<OntimeEvent, 'timeStart'>,
|
||||
previous?: Pick<OntimeEvent, 'timeStart' | 'duration'>,
|
||||
previous: Pick<OntimeEvent, 'timeStart' | 'duration'> | null,
|
||||
) {
|
||||
// if there is no previous there can't be a day offset
|
||||
if (!previous) {
|
||||
|
||||
@@ -1,61 +1,90 @@
|
||||
import { OntimeEvent, OntimeRundown, RundownCached, OntimeRundownEntry, PlayableEvent } from 'ontime-types';
|
||||
import { filterPlayable, filterTimedEvents } from 'ontime-utils';
|
||||
import {
|
||||
OntimeEvent,
|
||||
Rundown,
|
||||
OntimeEntry,
|
||||
PlayableEvent,
|
||||
EntryId,
|
||||
RundownEntries,
|
||||
ProjectRundowns,
|
||||
} from 'ontime-types';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
|
||||
/**
|
||||
* returns the normalised rundown
|
||||
* returns entire unfiltered rundown
|
||||
*/
|
||||
export function getNormalisedRundown(): RundownCached {
|
||||
return cache.get();
|
||||
export function getCurrentRundown(): Rundown {
|
||||
return cache.getCurrentRundown();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns entire unfiltered rundown
|
||||
* returns the the project rundown and the order arrays
|
||||
*/
|
||||
export function getRundown(): OntimeRundown {
|
||||
return cache.getPersistedRundown();
|
||||
export function getRundownData() {
|
||||
return {
|
||||
rundown: cache.getCurrentRundown(),
|
||||
rundownOrder: cache.getEventOrder(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all events of type OntimeEvent
|
||||
*/
|
||||
export function getTimedEvents(): OntimeEvent[] {
|
||||
return filterTimedEvents(getRundown());
|
||||
const { entries } = cache.get();
|
||||
const { timedEventsOrder } = cache.getEventOrder();
|
||||
return makeFlatRundownFromOrder(timedEventsOrder, entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all events that can be loaded
|
||||
* Utility flattens a normalised rundown
|
||||
*/
|
||||
export function getPlayableEvents(): PlayableEvent[] {
|
||||
return filterPlayable(getRundown());
|
||||
function makeFlatRundownFromOrder<T>(order: EntryId[], events: RundownEntries): T[] {
|
||||
return order.map((id) => events[id] as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an event given its index after filtering for OntimeEvents
|
||||
*/
|
||||
export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||
const timedEvents = getTimedEvents();
|
||||
return timedEvents.at(eventIndex);
|
||||
const { timedEventsOrder } = cache.getEventOrder();
|
||||
const eventId = timedEventsOrder[eventIndex];
|
||||
|
||||
if (!eventId) {
|
||||
return undefined;
|
||||
}
|
||||
const { entries } = getCurrentRundown();
|
||||
return entries[eventId] as OntimeEvent | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns first event that matches a given ID
|
||||
*/
|
||||
export function getEventWithId(eventId: string): OntimeRundownEntry | undefined {
|
||||
const rundown = getRundown();
|
||||
return rundown.find((event) => event.id === eventId);
|
||||
export function getEventWithId(eventId: string): OntimeEntry | undefined {
|
||||
const { entries } = getCurrentRundown();
|
||||
return entries[eventId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility returns the first playable event in rundown
|
||||
*/
|
||||
export function getFirstPlayable(playableOrder: EntryId[]): PlayableEvent | undefined {
|
||||
const firstEventId = playableOrder.at(0);
|
||||
if (!firstEventId) return;
|
||||
return getEventWithId(firstEventId) as PlayableEvent | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns first event that matches a given cue
|
||||
*/
|
||||
export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): OntimeEvent | undefined {
|
||||
const playableEvents = getPlayableEvents();
|
||||
const { playableEventsOrder } = cache.getEventOrder();
|
||||
|
||||
const lowerCaseCue = targetCue.toLowerCase();
|
||||
|
||||
for (let i = currentEventIndex; i < playableEvents.length; i++) {
|
||||
const event = playableEvents.at(i);
|
||||
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) {
|
||||
const eventId = playableEventsOrder[i];
|
||||
const event = getEventWithId(eventId) as PlayableEvent | undefined;
|
||||
if (event?.cue.toLowerCase() === lowerCaseCue) {
|
||||
return event;
|
||||
}
|
||||
@@ -65,39 +94,74 @@ export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): O
|
||||
/**
|
||||
* finds the previous event
|
||||
*/
|
||||
export function findPrevious(currentEventId?: string): OntimeEvent | null {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents || !playableEvents.length) {
|
||||
return null;
|
||||
export function findPrevious(currentEventId?: string): OntimeEvent | undefined {
|
||||
const { playableEventsOrder } = cache.getEventOrder();
|
||||
|
||||
if (!playableEventsOrder.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return playableEvents.at(0) ?? null;
|
||||
return getFirstPlayable(playableEventsOrder);
|
||||
}
|
||||
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
|
||||
const newIndex = Math.max(currentIndex - 1, 0);
|
||||
const previousEvent = playableEvents.at(newIndex) ?? null;
|
||||
return previousEvent;
|
||||
const previousEventId = playableEventsOrder.at(newIndex);
|
||||
|
||||
if (!previousEventId) {
|
||||
return getFirstPlayable(playableEventsOrder);
|
||||
}
|
||||
|
||||
return getEventWithId(previousEventId) as PlayableEvent | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the next event
|
||||
*/
|
||||
export function findNext(currentEventId?: string): PlayableEvent | null {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents.length) {
|
||||
return null;
|
||||
export function findNext(currentEventId?: string): PlayableEvent | undefined {
|
||||
const { playableEventsOrder } = cache.getEventOrder();
|
||||
|
||||
if (!playableEventsOrder.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return playableEvents.at(0) ?? null;
|
||||
return getFirstPlayable(playableEventsOrder);
|
||||
}
|
||||
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = currentIndex + 1;
|
||||
const nextEvent = playableEvents.at(newIndex);
|
||||
return nextEvent ?? null;
|
||||
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
|
||||
const newIndex = Math.min(currentIndex + 1, playableEventsOrder.length - 1);
|
||||
const nextEventId = playableEventsOrder.at(newIndex);
|
||||
|
||||
if (!nextEventId) {
|
||||
return getFirstPlayable(playableEventsOrder);
|
||||
}
|
||||
|
||||
return getEventWithId(nextEventId) as PlayableEvent | undefined;
|
||||
}
|
||||
|
||||
export function filterTimedEvents(rundown: Rundown, timedEventOrder: EntryId[]): OntimeEvent[] {
|
||||
return timedEventOrder.map((id) => rundown.entries[id] as OntimeEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first rundown in the project
|
||||
* We ensure that the projects always have a rundown
|
||||
*/
|
||||
export function getFirstRundown(rundowns: ProjectRundowns): Rundown {
|
||||
const firstKey = Object.keys(rundowns)[0];
|
||||
return rundowns[firstKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rundown given its ID
|
||||
*/
|
||||
export function getRundownOrThrow(rundowns: ProjectRundowns, rundownId: string): Rundown {
|
||||
if (!rundowns[rundownId]) {
|
||||
throw new Error(`Rundown with ID ${rundownId} not found`);
|
||||
}
|
||||
return rundowns[rundownId];
|
||||
}
|
||||
|
||||
@@ -31,13 +31,15 @@ import {
|
||||
getEventAtIndex,
|
||||
getNextEventWithCue,
|
||||
getEventWithId,
|
||||
getRundown,
|
||||
getCurrentRundown,
|
||||
getTimedEvents,
|
||||
getRundownData,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
import { triggerAutomations } from '../../api-data/automation/automation.service.js';
|
||||
import { getEventOrder } from '../rundown-service/rundownCache.js';
|
||||
|
||||
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow' | 'publicEventNow' | 'publicEventNext'>;
|
||||
|
||||
@@ -270,8 +272,9 @@ class RuntimeService {
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.updateLoaded(eventNow);
|
||||
} else {
|
||||
const rundown = getRundown();
|
||||
runtimeState.updateAll(rundown);
|
||||
const rundown = getCurrentRundown();
|
||||
const { timedEventsOrder } = getEventOrder();
|
||||
runtimeState.updateAll(rundown, timedEventsOrder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -298,8 +301,8 @@ class RuntimeService {
|
||||
}
|
||||
const previousState = runtimeState.getState();
|
||||
|
||||
const rundown = getRundown();
|
||||
const success = runtimeState.load(event, rundown, initialData);
|
||||
const { rundown, rundownOrder } = getRundownData();
|
||||
const success = runtimeState.load(event, rundown, rundownOrder.timedEventsOrder, initialData);
|
||||
|
||||
if (success) {
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||
@@ -583,9 +586,11 @@ class RuntimeService {
|
||||
* Handles special case to call roll on a loaded event which we do not want to discard
|
||||
*/
|
||||
private rollLoaded(offset?: number) {
|
||||
const rundown = getRundown();
|
||||
const rundown = getCurrentRundown();
|
||||
const { timedEventsOrder } = getEventOrder();
|
||||
|
||||
try {
|
||||
runtimeState.roll(rundown, offset);
|
||||
runtimeState.roll(rundown, timedEventsOrder, offset);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Server, `Roll: ${error}`);
|
||||
}
|
||||
@@ -605,8 +610,8 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
try {
|
||||
const rundown = getRundown();
|
||||
const result = runtimeState.roll(rundown);
|
||||
const { rundown, rundownOrder } = getRundownData();
|
||||
const result = runtimeState.roll(rundown, rundownOrder.timedEventsOrder);
|
||||
const newState = runtimeState.getState();
|
||||
if (result.eventId !== previousState.eventNow?.id) {
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
|
||||
@@ -657,8 +662,8 @@ class RuntimeService {
|
||||
return;
|
||||
}
|
||||
|
||||
const rundown = getRundown();
|
||||
runtimeState.resume(restorePoint, event, rundown);
|
||||
const { rundown, rundownOrder } = getRundownData();
|
||||
runtimeState.resume(restorePoint, event, rundown, rundownOrder.timedEventsOrder);
|
||||
logger.info(LogOrigin.Playback, 'Resuming playback');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @link https://developers.google.com/identity/protocols/oauth2/limited-input-device
|
||||
*/
|
||||
|
||||
import { AuthenticationStatus, CustomFields, LogOrigin, MaybeString, OntimeRundown } from 'ontime-types';
|
||||
import { AuthenticationStatus, CustomFields, DatabaseModel, LogOrigin, MaybeString, Rundown } from 'ontime-types';
|
||||
import { ImportMap, getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { sheets, type sheets_v4 } from '@googleapis/sheets';
|
||||
@@ -13,8 +13,8 @@ import got from 'got';
|
||||
|
||||
import { parseExcel } from '../../utils/parser.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { parseRundown } from '../../utils/parserFunctions.js';
|
||||
import { getRundown } from '../rundown-service/rundownUtils.js';
|
||||
import { parseRundowns } from '../../utils/parserFunctions.js';
|
||||
import { getCurrentRundown, getRundownOrThrow } from '../rundown-service/rundownUtils.js';
|
||||
import { getCustomFields } from '../rundown-service/rundownCache.js';
|
||||
|
||||
import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js';
|
||||
@@ -292,8 +292,8 @@ export async function upload(sheetId: string, options: ImportMap) {
|
||||
throw new Error(`Sheet read failed: ${readResponse.statusText}`);
|
||||
}
|
||||
|
||||
const { rundownMetadata } = parseExcel(readResponse.data.values, getCustomFields(), options);
|
||||
const rundown = getRundown();
|
||||
const { rundownMetadata } = parseExcel(readResponse.data.values, getCustomFields(), 'not-used', options);
|
||||
const rundown = getCurrentRundown();
|
||||
const titleRow = Object.values(rundownMetadata)[0]['row'];
|
||||
const updateRundown = Array<sheets_v4.Schema$Request>();
|
||||
|
||||
@@ -322,16 +322,17 @@ export async function upload(sheetId: string, options: ImportMap) {
|
||||
range: {
|
||||
dimension: 'ROWS',
|
||||
startIndex: titleRow + 1,
|
||||
endIndex: titleRow + rundown.length,
|
||||
endIndex: titleRow + rundown.order.length,
|
||||
sheetId: worksheetId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// update the corresponding row with event data
|
||||
rundown.forEach((entry, index) =>
|
||||
updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, rundownMetadata)),
|
||||
);
|
||||
rundown.order.forEach((entryId, index) => {
|
||||
const entry = rundown.entries[entryId];
|
||||
return updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, rundownMetadata));
|
||||
});
|
||||
|
||||
const writeResponse = await sheets({ version: 'v4', auth: currentAuthClient }).spreadsheets.batchUpdate({
|
||||
spreadsheetId: sheetId,
|
||||
@@ -353,7 +354,7 @@ export async function download(
|
||||
sheetId: string,
|
||||
options: ImportMap,
|
||||
): Promise<{
|
||||
rundown: OntimeRundown;
|
||||
rundown: Rundown;
|
||||
customFields: CustomFields;
|
||||
}> {
|
||||
const { range } = await verifyWorksheet(sheetId, options.worksheet);
|
||||
@@ -369,10 +370,19 @@ export async function download(
|
||||
throw new Error(`Sheet read failed: ${googleResponse.statusText}`);
|
||||
}
|
||||
|
||||
const dataFromSheet = parseExcel(googleResponse.data.values, getCustomFields(), options);
|
||||
const { customFields, rundown } = parseRundown(dataFromSheet);
|
||||
if (rundown.length < 1) {
|
||||
const dataFromSheet = parseExcel(googleResponse.data.values, getCustomFields(), 'Rundown', options);
|
||||
|
||||
const rundownId = dataFromSheet.rundown.id;
|
||||
const dataModel: Pick<DatabaseModel, 'rundowns' | 'customFields'> = {
|
||||
rundowns: {
|
||||
[rundownId]: dataFromSheet.rundown,
|
||||
},
|
||||
customFields: dataFromSheet.customFields,
|
||||
};
|
||||
const { customFields, rundowns } = parseRundowns(dataModel);
|
||||
const rundown = getRundownOrThrow(rundowns, rundownId);
|
||||
if (rundown.order.length < 1) {
|
||||
throw new Error('Sheet: Could not find data to import in the worksheet');
|
||||
}
|
||||
return { rundown, customFields };
|
||||
return { rundown: rundowns[rundownId], customFields };
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
@@ -84,6 +85,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
@@ -134,6 +136,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
@@ -186,6 +189,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
@@ -218,6 +222,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
@@ -254,6 +259,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
currentBlock: null,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeEvent, OntimeEntry } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import type { sheets_v4 } from '@googleapis/sheets';
|
||||
@@ -74,14 +74,14 @@ export function getA1Notation(row: number, column: number): string {
|
||||
|
||||
/**
|
||||
* @description - creates updateCells request from ontime event
|
||||
* @param {OntimeRundownEntry} event
|
||||
* @param {OntimeEntry} event
|
||||
* @param {number} index - index of the event
|
||||
* @param {number} worksheetId
|
||||
* @param {object} metadata - object with all the cell positions of the title of each attribute
|
||||
* @returns {sheets_v4.Schema} - list of update requests
|
||||
*/
|
||||
export function cellRequestFromEvent(
|
||||
event: OntimeRundownEntry,
|
||||
event: OntimeEntry,
|
||||
index: number,
|
||||
worksheetId: number,
|
||||
metadata: object,
|
||||
@@ -125,7 +125,7 @@ export function cellRequestFromEvent(
|
||||
};
|
||||
}
|
||||
|
||||
function getCellData(key: keyof OntimeEvent | 'blank', event: OntimeRundownEntry) {
|
||||
function getCellData(key: keyof OntimeEvent | 'blank', event: OntimeEntry) {
|
||||
if (isOntimeEvent(event)) {
|
||||
if (key === 'blank') {
|
||||
return {};
|
||||
|
||||
Reference in New Issue
Block a user