mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
refactor: update timers (#729)
* refactor: remove duplication * refactor: previous times * refactor: event patching
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { LogOrigin, OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { LogOrigin, OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, isOntimeEvent } from 'ontime-types';
|
||||
import { generateId, getCueCandidate } from 'ontime-utils';
|
||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
delayedRundownCacheKey,
|
||||
} from './delayedRundown.utils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { validateEvent } from '../../utils/parser.js';
|
||||
import { createEvent } from '../../utils/parser.js';
|
||||
import { stateMutations } from '../../state.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
|
||||
switch (eventData.type) {
|
||||
case SupportedEvent.Event: {
|
||||
newEvent = validateEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
||||
newEvent = createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
||||
break;
|
||||
}
|
||||
case SupportedEvent.Delay:
|
||||
@@ -80,7 +80,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
}
|
||||
|
||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||
if (eventData.type === SupportedEvent.Event && eventData?.cue === '') {
|
||||
if (isOntimeEvent(eventData) && eventData?.cue === '') {
|
||||
throw new Error('Cue value invalid');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||
import { isProduction } from '../../setup.js';
|
||||
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
||||
import { _applyDelay } from '../delayUtils.js';
|
||||
import { createPatch } from '../../utils/parser.js';
|
||||
|
||||
/**
|
||||
* Keep incremental revision number of rundown for runtime
|
||||
@@ -109,6 +110,16 @@ export async function cachedEdit(
|
||||
eventId: string,
|
||||
patchObject: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>,
|
||||
) {
|
||||
const makeEvent = (eventFromRundown: OntimeRundownEntry): OntimeRundownEntry => {
|
||||
if (isOntimeEvent(eventFromRundown)) {
|
||||
const newEvent = createPatch(eventFromRundown, patchObject as OntimeEvent);
|
||||
newEvent.revision++;
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
return { ...eventFromRundown, ...patchObject } as OntimeRundownEntry;
|
||||
};
|
||||
|
||||
const indexInMemory = DataProvider.getIndexOf(eventId);
|
||||
if (indexInMemory < 0) {
|
||||
throw new Error('No event with ID found');
|
||||
@@ -125,10 +136,7 @@ export async function cachedEdit(
|
||||
return eventFromRundown;
|
||||
}
|
||||
|
||||
const newEvent = { ...eventFromRundown, ...patchObject } as OntimeRundownEntry;
|
||||
if (isOntimeEvent(newEvent)) {
|
||||
newEvent.revision++;
|
||||
}
|
||||
const newEvent = makeEvent(eventFromRundown);
|
||||
updatedRundown[indexInMemory] = newEvent;
|
||||
|
||||
let newDelayedRundown = getDelayedRundown();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { vi } from 'vitest';
|
||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { parseExcel, parseJson, validateEvent } from '../parser.js';
|
||||
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
||||
import { makeString } from '../parserUtils.js';
|
||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('test parser edge cases', () => {
|
||||
};
|
||||
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: undefined event type, skipping');
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
|
||||
expect(parseResponse?.rundown.length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -464,7 +464,7 @@ describe('test event validator', () => {
|
||||
const event = {
|
||||
title: 'test',
|
||||
};
|
||||
const validated = validateEvent(event, 'test');
|
||||
const validated = createEvent(event, 'test');
|
||||
|
||||
expect(validated).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -497,7 +497,7 @@ describe('test event validator', () => {
|
||||
|
||||
it('fails an empty object', () => {
|
||||
const event = {};
|
||||
const validated = validateEvent(event, 'none');
|
||||
const validated = createEvent(event, 'none');
|
||||
expect(validated).toEqual(null);
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ describe('test event validator', () => {
|
||||
note: '1899-12-30T08:00:10.000Z',
|
||||
};
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const validated = validateEvent(event, 'not-used');
|
||||
const validated = createEvent(event, 'not-used');
|
||||
expect(typeof validated.title).toEqual('string');
|
||||
expect(typeof validated.subtitle).toEqual('string');
|
||||
expect(typeof validated.presenter).toEqual('string');
|
||||
@@ -522,7 +522,7 @@ describe('test event validator', () => {
|
||||
timeEnd: '2',
|
||||
};
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const validated = validateEvent(event);
|
||||
const validated = createEvent(event);
|
||||
expect(typeof validated.timeStart).toEqual('number');
|
||||
expect(validated.timeStart).toEqual(0);
|
||||
expect(typeof validated.timeEnd).toEqual('number');
|
||||
@@ -534,7 +534,7 @@ describe('test event validator', () => {
|
||||
title: {},
|
||||
};
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const validated = validateEvent(event);
|
||||
const validated = createEvent(event);
|
||||
expect(typeof validated.title).toEqual('string');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,20 +43,25 @@ import { coerceBoolean } from './coerceType.js';
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
export const JSON_MIME = 'application/json';
|
||||
|
||||
type ExcelData = Pick<DatabaseModel, 'rundown' | 'project' | 'userFields'> & {
|
||||
projectMetadata: Record<string, { row: number; col: number }>;
|
||||
rundownMetadata: Record<string, { row: number; col: number }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Excel array parser
|
||||
* @param {array} excelData - array with excel sheet
|
||||
* @param {ExcelImportOptions} options - an object that contains the import map
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>): ExcelData => {
|
||||
const projectMetadata = {};
|
||||
const rundownMetadata = {};
|
||||
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
||||
for (const [key, value] of Object.entries(importMap)) {
|
||||
importMap[key] = value.toLocaleLowerCase();
|
||||
}
|
||||
const projectData: Partial<ProjectData> = {
|
||||
const projectData: ProjectData = {
|
||||
title: '',
|
||||
description: '',
|
||||
publicUrl: '',
|
||||
@@ -64,7 +69,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
backstageUrl: '',
|
||||
backstageInfo: '',
|
||||
};
|
||||
const customUserFields: Partial<UserFields> = {
|
||||
const customUserFields: UserFields = {
|
||||
user0: importMap.user0,
|
||||
user1: importMap.user1,
|
||||
user2: importMap.user2,
|
||||
@@ -350,10 +355,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
return {
|
||||
rundown,
|
||||
project: projectData,
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
},
|
||||
userFields: customUserFields,
|
||||
projectMetadata,
|
||||
rundownMetadata,
|
||||
@@ -393,61 +394,66 @@ export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
return returnData as DatabaseModel;
|
||||
};
|
||||
|
||||
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
|
||||
if (Object.keys(patchEvent).length === 0) {
|
||||
return originalEvent;
|
||||
}
|
||||
|
||||
const { timeStart, timeEnd, duration } = validateTimes(
|
||||
patchEvent?.timeStart ?? originalEvent.timeStart,
|
||||
patchEvent?.timeEnd ?? originalEvent.timeEnd,
|
||||
patchEvent?.duration ?? originalEvent.duration,
|
||||
);
|
||||
|
||||
return {
|
||||
id: originalEvent.id,
|
||||
type: SupportedEvent.Event,
|
||||
title: makeString(patchEvent.title, originalEvent.title),
|
||||
subtitle: makeString(patchEvent.subtitle, originalEvent.subtitle),
|
||||
presenter: makeString(patchEvent.presenter, originalEvent.presenter),
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
endAction: validateEndAction(patchEvent.endAction, EndAction.None),
|
||||
timerType: validateTimerType(patchEvent.timerType, TimerType.CountDown),
|
||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
||||
note: makeString(patchEvent.note, originalEvent.note),
|
||||
user0: makeString(patchEvent.user0, originalEvent.user0),
|
||||
user1: makeString(patchEvent.user1, originalEvent.user1),
|
||||
user2: makeString(patchEvent.user2, originalEvent.user2),
|
||||
user3: makeString(patchEvent.user3, originalEvent.user3),
|
||||
user4: makeString(patchEvent.user4, originalEvent.user4),
|
||||
user5: makeString(patchEvent.user5, originalEvent.user5),
|
||||
user6: makeString(patchEvent.user6, originalEvent.user6),
|
||||
user7: makeString(patchEvent.user7, originalEvent.user7),
|
||||
user8: makeString(patchEvent.user8, originalEvent.user8),
|
||||
user9: makeString(patchEvent.user9, originalEvent.user9),
|
||||
colour: makeString(patchEvent.colour, originalEvent.colour),
|
||||
cue: makeString(patchEvent.cue, originalEvent.cue),
|
||||
revision: originalEvent.revision,
|
||||
timeWarning: patchEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Enforces formatting for events
|
||||
* @param {object} eventArgs - attributes of event
|
||||
* @param cueFallback
|
||||
* @returns {object|null} - formatted object or null in case is invalid
|
||||
*/
|
||||
|
||||
export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string) => {
|
||||
// ensure id is defined and unique
|
||||
const id = eventArgs.id || generateId();
|
||||
|
||||
let event = null;
|
||||
|
||||
// return if object is empty
|
||||
if (Object.keys(eventArgs).length > 0) {
|
||||
// make sure all properties exits
|
||||
// dont load any extra properties than the ones known
|
||||
|
||||
const e = eventArgs;
|
||||
const d = eventDef;
|
||||
|
||||
const { timeStart, timeEnd, duration } = validateTimes(e.timeStart, e.timeEnd, e.duration);
|
||||
|
||||
event = {
|
||||
...d,
|
||||
title: makeString(e.title, d.title),
|
||||
subtitle: makeString(e.subtitle, d.subtitle),
|
||||
presenter: makeString(e.presenter, d.presenter),
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
endAction: validateEndAction(e.endAction, EndAction.None),
|
||||
timerType: validateTimerType(e.timerType, TimerType.CountDown),
|
||||
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
||||
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
||||
note: makeString(e.note, d.note),
|
||||
user0: makeString(e.user0, d.user0),
|
||||
user1: makeString(e.user1, d.user1),
|
||||
user2: makeString(e.user2, d.user2),
|
||||
user3: makeString(e.user3, d.user3),
|
||||
user4: makeString(e.user4, d.user4),
|
||||
user5: makeString(e.user5, d.user5),
|
||||
user6: makeString(e.user6, d.user6),
|
||||
user7: makeString(e.user7, d.user7),
|
||||
user8: makeString(e.user8, d.user8),
|
||||
user9: makeString(e.user9, d.user9),
|
||||
colour: makeString(e.colour, d.colour),
|
||||
cue: makeString(e.cue, cueFallback),
|
||||
id,
|
||||
type: 'event',
|
||||
timeWarning: e.timeWarning,
|
||||
timeDanger: e.timeDanger,
|
||||
};
|
||||
export const createEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string): OntimeEvent | null => {
|
||||
if (Object.keys(eventArgs).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseEvent = {
|
||||
id: eventArgs?.id ?? generateId(),
|
||||
cue: cueFallback,
|
||||
...eventDef,
|
||||
};
|
||||
const event = createPatch(baseEvent, eventArgs);
|
||||
return event;
|
||||
};
|
||||
|
||||
@@ -497,7 +503,6 @@ export const fileHandler = async (file: string, options: ExcelImportOptions): Pr
|
||||
}
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
// if json check version
|
||||
const rawdata = fs.readFileSync(file).toString();
|
||||
let uploadedJson = null;
|
||||
|
||||
|
||||
@@ -13,11 +13,15 @@ import {
|
||||
HttpSubscription,
|
||||
OscSubscriptionOptions,
|
||||
HttpSubscriptionOptions,
|
||||
DatabaseModel,
|
||||
isOntimeEvent,
|
||||
isOntimeDelay,
|
||||
isOntimeBlock,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { validateEvent } from './parser.js';
|
||||
import { createEvent } from './parser.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
|
||||
/**
|
||||
@@ -25,7 +29,7 @@ import { MAX_EVENTS } from '../settings.js';
|
||||
* @param {object} data - data object
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseRundown = (data): OntimeRundown => {
|
||||
export const parseRundown = (data: Partial<DatabaseModel>): OntimeRundown => {
|
||||
let newRundown: OntimeRundown = [];
|
||||
if ('rundown' in data) {
|
||||
console.log('Found rundown definition, importing...');
|
||||
@@ -33,7 +37,7 @@ export const parseRundown = (data): OntimeRundown => {
|
||||
try {
|
||||
let eventIndex = 0;
|
||||
const ids = [];
|
||||
for (const e of data.rundown) {
|
||||
for (const event of data.rundown) {
|
||||
// cap number of events
|
||||
if (rundown.length >= MAX_EVENTS) {
|
||||
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
@@ -41,28 +45,28 @@ export const parseRundown = (data): OntimeRundown => {
|
||||
}
|
||||
|
||||
// double check unique ids
|
||||
if (ids.includes(e?.id)) {
|
||||
if (ids.includes(event?.id)) {
|
||||
console.log('ERROR: ID collision on import, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type === 'event') {
|
||||
if (isOntimeEvent(event)) {
|
||||
eventIndex += 1;
|
||||
const event = validateEvent(e, eventIndex.toString());
|
||||
const parsedEvent = createEvent(event, eventIndex.toString());
|
||||
if (event != null) {
|
||||
rundown.push(event);
|
||||
ids.push(event.id);
|
||||
rundown.push(parsedEvent);
|
||||
ids.push(parsedEvent.id);
|
||||
}
|
||||
} else if (e.type === 'delay') {
|
||||
} else if (isOntimeDelay(event)) {
|
||||
rundown.push({
|
||||
...delayDef,
|
||||
duration: e.duration,
|
||||
id: e.id || generateId(),
|
||||
duration: event.duration,
|
||||
id: event.id || generateId(),
|
||||
});
|
||||
} else if (e.type === 'block') {
|
||||
rundown.push({ ...blockDef, title: e.title, id: e.id || generateId() });
|
||||
} else if (isOntimeBlock(event)) {
|
||||
rundown.push({ ...blockDef, title: event.title, id: event.id || generateId() });
|
||||
} else {
|
||||
console.log('ERROR: undefined event type, skipping');
|
||||
console.log('ERROR: unkown event type, skipping');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -112,16 +116,16 @@ export const parseSettings = (data): Settings => {
|
||||
const s = data.settings;
|
||||
|
||||
// skip if file definition is missing
|
||||
if (s.app == null || s.version == null) {
|
||||
if (s?.app !== 'ontime' || s?.version == null) {
|
||||
console.log('ERROR: unknown app version, skipping');
|
||||
} else {
|
||||
const settings = {
|
||||
version: dbModel.settings.version,
|
||||
serverPort: s.serverPort || dbModel.settings.serverPort,
|
||||
editorKey: s.editorKey || null,
|
||||
operatorKey: s.operatorKey || null,
|
||||
timeFormat: s.timeFormat || '24',
|
||||
language: s.language || 'en',
|
||||
serverPort: s.serverPort ?? dbModel.settings.serverPort,
|
||||
editorKey: s.editorKey ?? null,
|
||||
operatorKey: s.operatorKey ?? null,
|
||||
timeFormat: s.timeFormat ?? '24',
|
||||
language: s.language ?? 'en',
|
||||
};
|
||||
|
||||
// write to db
|
||||
@@ -287,15 +291,15 @@ export const parseAliases = (data): Alias[] => {
|
||||
if ('aliases' in data) {
|
||||
console.log('Found Aliases definition, importing...');
|
||||
try {
|
||||
for (const a of data.aliases) {
|
||||
for (const alias of data.aliases) {
|
||||
const newAlias = {
|
||||
enabled: a.enabled || false,
|
||||
alias: a.alias || '',
|
||||
pathAndParams: a.pathAndParams || '',
|
||||
enabled: alias.enabled ?? false,
|
||||
alias: alias.alias ?? '',
|
||||
pathAndParams: alias.pathAndParams ?? '',
|
||||
};
|
||||
newAliases.push(newAlias);
|
||||
}
|
||||
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
|
||||
console.log(`Uploaded ${newAliases.length} alias(es)`);
|
||||
} catch (error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
@@ -328,4 +332,4 @@ export const parseUserFields = (data): UserFields => {
|
||||
}
|
||||
}
|
||||
return { ...newUserFields };
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user