refactor: extract time-to-end

This commit is contained in:
Carlos Valente
2024-12-15 21:25:41 +01:00
committed by Carlos Valente
parent 41fe213ebb
commit 0f3feca7ab
32 changed files with 419 additions and 248 deletions
+84 -47
View File
@@ -463,7 +463,7 @@ describe('test event validator', () => {
const event = {
title: 'test',
};
const validated = createEvent(event, 'test');
const validated = createEvent(event, 1);
expect(validated).toEqual(
expect.objectContaining({
@@ -471,12 +471,13 @@ describe('test event validator', () => {
note: expect.any(String),
timeStart: expect.any(Number),
timeEnd: expect.any(Number),
isTimeToEnd: expect.any(Boolean),
isPublic: expect.any(Boolean),
skip: expect.any(Boolean),
revision: expect.any(Number),
type: expect.any(String),
id: expect.any(String),
cue: 'test',
cue: '2',
colour: expect.any(String),
custom: expect.any(Object),
}),
@@ -485,7 +486,7 @@ describe('test event validator', () => {
it('fails an empty object', () => {
const event = {};
const validated = createEvent(event, 'none');
const validated = createEvent(event, 1);
expect(validated).toEqual(null);
});
@@ -495,7 +496,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 = createEvent(event, 'not-used');
const validated = createEvent(event, 1);
if (validated === null) {
throw new Error('unexpected value');
}
@@ -1659,7 +1660,7 @@ describe('parseExcel()', () => {
expect((events.at(0) as OntimeEvent).colour).toEqual('#F00'); //<--trailing white space in Excel data
});
it('link start', () => {
it('parses link start and checks that is applicable', () => {
const testData = [
[
'Time Start',
@@ -1675,11 +1676,11 @@ describe('parseExcel()', () => {
'Timer type',
],
['4:30:00', '9:45:00', 'A', 'load-next', '', '', 'Rainbow chase', '#F00', 102, '', 'count-down'],
['9:45:00', '10:56:00', 'C', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'],
['10:00:00', '16:36:00', 'D', 'load-next', 'x', '', 'Rainbow chase', '#F00', 102, 'x', 'count-down'], //<-- incorrect start times are overridden
['21:45:00', '22:56:00', 'E', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, '', 'count-down'],
['9:45:00', '10:56:00', 'B', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'],
['10:00:00', '16:36:00', 'C', 'load-next', 'x', '', 'Rainbow chase', '#F00', 102, 'x', 'count-down'], // <-- incorrect start times are overridden
['21:45:00', '22:56:00', 'D', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, '', 'count-down'],
['', '', 'BLOCK', '', '', '', '', '', '', '', 'block'],
['00:0:00', '23:56:00', 'G', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'], //<-- link past blocks
['00:0:00', '23:56:00', 'E', 'load-next', 'x', '', 'Rainbow chase', '#0F0', 103, 'x', 'count-down'], // <-- link past blocks
[],
];
@@ -1709,30 +1710,46 @@ describe('parseExcel()', () => {
const { rundown, order } = cache.get();
const firstId = order.at(0); // A
const secondId = order.at(1); // C
const thirdId = order.at(2); // D
const fourthId = order.at(3); // E
const fifhtId = order.at(4); // Block
const sixthId = order.at(5); // G
const secondId = order.at(1); // B
const thirdId = order.at(2); // C
const fourthId = order.at(3); // D
const fifthId = order.at(4); // Block
const sixthId = order.at(5); // E
if (!firstId || !secondId || !thirdId || !fourthId || !fifhtId || !sixthId) {
if (!firstId || !secondId || !thirdId || !fourthId || !fifthId || !sixthId) {
throw new Error('Unexpected value');
}
expect((rundown[firstId] as OntimeEvent).timeStart).toEqual(16200000);
expect((rundown[secondId] as OntimeEvent).timeStart).toEqual((rundown[firstId] as OntimeEvent).timeEnd);
expect((rundown[secondId] as OntimeEvent).linkStart).toEqual((rundown[firstId] as OntimeEvent).id);
expect((rundown[thirdId] as OntimeEvent).timeStart).toEqual((rundown[secondId] as OntimeEvent).timeEnd);
expect((rundown[thirdId] as OntimeEvent).linkStart).toEqual((rundown[secondId] as OntimeEvent).id);
expect((rundown[fourthId] as OntimeEvent).timeStart).toEqual(78300000);
expect((rundown[fifhtId] as OntimeEvent).type).toEqual(SupportedEvent.Block);
expect((rundown[sixthId] as OntimeEvent).timeStart).toEqual((rundown[fourthId] as OntimeEvent).timeEnd);
expect((rundown[sixthId] as OntimeEvent).linkStart).toEqual((rundown[fourthId] as OntimeEvent).id);
expect(rundown).toMatchObject({
[firstId]: {
title: 'A',
timeStart: 16200000,
},
[secondId]: {
title: 'B',
timeStart: (rundown[firstId] as OntimeEvent).timeEnd,
linkStart: (rundown[firstId] as OntimeEvent).id,
},
[thirdId]: {
title: 'C',
timeStart: (rundown[secondId] as OntimeEvent).timeEnd,
linkStart: (rundown[secondId] as OntimeEvent).id,
},
[fourthId]: {
title: 'D',
timeStart: 78300000,
linkStart: null,
},
[fifthId]: {
title: 'BLOCK',
type: SupportedEvent.Block,
},
[sixthId]: {
title: 'E',
timeStart: (rundown[fourthId] as OntimeEvent).timeEnd,
linkStart: (rundown[fourthId] as OntimeEvent).id,
},
});
});
it('#971 BUG: parses time fields and booleans', () => {
@@ -1762,7 +1779,7 @@ describe('parseExcel()', () => {
'false',
'Setup',
'',
'time-to-end',
'count-down',
'none',
'15',
'00:05:00',
@@ -1778,7 +1795,7 @@ describe('parseExcel()', () => {
'false',
'Meeting 1',
'',
'time-to-end',
'count-down',
'none',
15,
'00:05:00',
@@ -1794,7 +1811,7 @@ describe('parseExcel()', () => {
'false',
'Meeting 2',
'',
'time-to-end',
'count-down',
'none',
'13',
'5',
@@ -1810,7 +1827,7 @@ describe('parseExcel()', () => {
'true',
'Lunch',
'',
'time-to-end',
'count-down',
'none',
13,
5,
@@ -1839,26 +1856,46 @@ describe('parseExcel()', () => {
const parsedData = parseExcel(testData, {});
const { rundown } = parsedData;
// '15' as a string is parsed by smart time entry as minutes
expect(rundown[0]).toMatchObject({
cue: 'SETUP',
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// elements in bug report
// 15 is a number, in which case we parse it as a minutes value
expect((rundown.at(1) as OntimeEvent).timeWarning).toBe(15 * MILLIS_PER_MINUTE);
expect(rundown[1]).toMatchObject({
cue: 'MEET1',
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// in the case where a string is passed, we need to check whether it is an ISO 8601 date
expect((rundown.at(2) as OntimeEvent).duration).toBe(60 * MILLIS_PER_MINUTE);
expect((rundown.at(2) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect(rundown[2]).toMatchObject({
cue: 'MEET2',
duration: 60 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(3) as OntimeEvent).timeWarning).toBe(13 * MILLIS_PER_MINUTE);
expect((rundown.at(3) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect(rundown[3]).toMatchObject({
cue: 'lunch',
timeWarning: 13 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(4) as OntimeEvent).duration).toBe(90 * MILLIS_PER_MINUTE);
expect((rundown.at(4) as OntimeEvent).linkStart).toBe(false);
expect((rundown.at(4) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE);
expect((rundown.at(4) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect(rundown[4]).toMatchObject({
cue: 'MEET3',
duration: 90 * MILLIS_PER_MINUTE,
linkStart: false,
timeWarning: 11 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect((rundown.at(5) as OntimeEvent).duration).toBe(30 * MILLIS_PER_MINUTE);
// if we get a boolean, we should just use that
expect((rundown.at(5) as OntimeEvent).linkStart).toBe(true);
expect((rundown.at(5) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE);
expect(rundown[5]).toMatchObject({
cue: 'MEET4',
duration: 30 * MILLIS_PER_MINUTE,
timeWarning: 11 * MILLIS_PER_MINUTE,
// if we get a boolean, we should just use that
linkStart: true,
});
});
});
@@ -405,28 +405,6 @@ describe('sanitiseCustomFields()', () => {
});
describe('parseRundown() linking', () => {
const blankEvent: OntimeEvent = {
id: '',
type: SupportedEvent.Event,
cue: '',
title: '',
note: '',
endAction: EndAction.None,
timerType: TimerType.CountDown,
linkStart: null,
timeStrategy: TimeStrategy.LockDuration,
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
};
it('returns linked events', () => {
const data: Partial<DatabaseModel> = {
rundown: [
@@ -445,12 +423,11 @@ describe('parseRundown() linking', () => {
customFields: {},
};
const expected: OntimeRundown = [
{ ...blankEvent, id: '1', cue: '0' },
{ ...blankEvent, id: '2', cue: '1', linkStart: '1' },
];
const result = parseRundown(data);
expect(result.rundown).toEqual(expected);
expect(result.rundown[1]).toMatchObject({
id: '2',
linkStart: '1',
});
});
it('returns unlinked if no previous', () => {
@@ -466,9 +443,11 @@ describe('parseRundown() linking', () => {
customFields: {},
};
const expected: OntimeRundown = [{ ...blankEvent, id: '2', cue: '0' }];
const result = parseRundown(data);
expect(result.rundown).toEqual(expected);
expect(result.rundown[0]).toMatchObject({
id: '2',
linkStart: null,
});
});
it('returns linked events past blocks and delays', () => {
@@ -505,14 +484,65 @@ describe('parseRundown() linking', () => {
customFields: {},
};
const expected: OntimeRundown = [
{ ...blankEvent, id: '1', cue: '0' },
{ id: 'delay1', type: SupportedEvent.Delay, duration: 0 },
{ ...blankEvent, id: '2', cue: '1', linkStart: '1' },
{ id: 'block1', type: SupportedEvent.Block, title: '' },
{ ...blankEvent, id: '3', cue: '2', linkStart: '2' },
];
const result = parseRundown(data);
expect(result.rundown).toEqual(expected);
expect(result.rundown[0]).toMatchObject({
id: '1',
cue: '1',
});
// skip delay
expect(result.rundown[2]).toMatchObject({
id: '2',
cue: '2',
linkStart: '1',
});
// skip block
expect(result.rundown[4]).toMatchObject({
id: '3',
cue: '3',
linkStart: '2',
});
});
});
describe('parseRundown() migrations', () => {
const legacyEvent = {
id: '1',
type: SupportedEvent.Event,
cue: '',
title: '',
note: '',
endAction: EndAction.None,
timerType: 'time-to-end',
linkStart: null,
timeStrategy: TimeStrategy.LockDuration,
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
};
it('migrates an event with time-to-end', () => {
const result = parseRundown({ rundown: [legacyEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
isTimeToEnd: true,
});
});
it('migrates an event without time-to-end', () => {
const countdownEvent = { ...legacyEvent, timerType: TimerType.CountDown };
const result = parseRundown({ rundown: [countdownEvent] as OntimeRundown });
expect(result.rundown[0]).toMatchObject({
id: '1',
timerType: TimerType.CountDown,
isTimeToEnd: false,
});
});
});
+11 -8
View File
@@ -13,8 +13,8 @@ import {
CustomFields,
DatabaseModel,
EventCustomFields,
isOntimeBlock,
LogOrigin,
OntimeBlock,
OntimeEvent,
OntimeRundown,
SupportedEvent,
@@ -271,10 +271,11 @@ export const parseExcel = (
// if any data was found in row, push to array
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
console.log('keys found ---->', event)
if (keysFound > 0) {
// if it is a Block type drop all other filed
if (event.type === SupportedEvent.Block) {
rundown.push({ type: event.type, id: event.id, title: event.title } as OntimeBlock);
if (isOntimeBlock(event)) {
rundown.push({ type: event.type, id: event.id, title: event.title });
} else {
if (timerTypeIndex === null) {
event.timerType = TimerType.CountDown;
@@ -360,7 +361,6 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
patchEvent?.duration ?? originalEvent.duration,
patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy),
);
const maybeLinkStart = patchEvent.linkStart !== undefined ? patchEvent.linkStart : originalEvent.linkStart;
return {
id: originalEvent.id,
@@ -370,9 +370,10 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
timeEnd,
duration,
timeStrategy,
linkStart: validateLinkStart(maybeLinkStart),
linkStart: validateLinkStart(patchEvent.linkStart),
endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction),
timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType),
isTimeToEnd: typeof patchEvent.isTimeToEnd === 'boolean' ? patchEvent.isTimeToEnd : originalEvent.isTimeToEnd,
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
note: makeString(patchEvent.note, originalEvent.note),
@@ -389,17 +390,19 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
/**
* @description Enforces formatting for events
* @param {object} eventArgs - attributes of event
* @param cueFallback
* @param {number} eventIndex - can be a string when we pass the a suggested cue name
* @returns {object|null} - formatted object or null in case is invalid
*/
export const createEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string): OntimeEvent | null => {
export const createEvent = (eventArgs: Partial<OntimeEvent>, eventIndex: number | string): OntimeEvent | null => {
if (Object.keys(eventArgs).length === 0) {
return null;
}
const cue = typeof eventIndex === 'number' ? String(eventIndex + 1) : eventIndex;
const baseEvent = {
id: eventArgs?.id ?? generateId(),
cue: cueFallback,
cue,
...eventDef,
};
const event = createPatch(baseEvent, eventArgs);
+27 -10
View File
@@ -12,6 +12,7 @@ import {
OscSubscription,
ProjectData,
Settings,
TimerType,
URLPreset,
ViewSettings,
isOntimeBlock,
@@ -19,13 +20,7 @@ import {
isOntimeDelay,
isOntimeEvent,
} from 'ontime-types';
import {
customFieldLabelToKey,
generateId,
getErrorMessage,
getLastEvent,
isAlphanumericWithSpace,
} from 'ontime-utils';
import { customFieldLabelToKey, generateId, getErrorMessage, isAlphanumericWithSpace } from 'ontime-utils';
import { dbModel } from '../models/dataModel.js';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
@@ -52,6 +47,7 @@ export function parseRundown(
const rundown: OntimeRundown = [];
let eventIndex = 0;
let previousId: string | null = null;
const ids: string[] = [];
for (const event of data.rundown) {
@@ -64,12 +60,13 @@ export function parseRundown(
let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
if (isOntimeEvent(event)) {
const maybeEvent = runEventMigrations({ ...event, id });
if (event.linkStart) {
const prevId = getLastEvent(rundown).lastEvent?.id ?? null;
event.linkStart = prevId;
maybeEvent.linkStart = previousId;
}
newEvent = createEvent(event, eventIndex.toString());
newEvent = createEvent(maybeEvent, eventIndex);
// skip if event is invalid
if (newEvent == null) {
emitError?.('Skipping event without payload');
@@ -84,6 +81,7 @@ export function parseRundown(
}
}
previousId = id;
eventIndex += 1;
} else if (isOntimeDelay(event)) {
newEvent = { ...delayDef, duration: event.duration, id };
@@ -349,3 +347,22 @@ export function sanitiseCustomFields(data: object): CustomFields {
return newCustomFields;
}
/**
* Time to end was moved from a TimerType to a standalone boolean
* Released as part of v3.10.0
*/
function migrateTimeToEnd(event: any): OntimeEvent {
if (event.timerType === 'time-to-end') {
event.timerType = TimerType.CountDown;
event.isTimeToEnd = true;
}
return event;
}
/**
* Mutating function migrates event data entries
*/
function runEventMigrations(event: any): OntimeEvent {
return migrateTimeToEnd(event);
}