refactor: restructure model to contain an object of rundowns

This commit is contained in:
Carlos Valente
2025-03-15 09:04:33 +01:00
committed by arc-alex
parent 0a80d6db31
commit abd9b127db
111 changed files with 4357 additions and 4515 deletions
+1 -4
View File
@@ -36,11 +36,8 @@ export function deleteAtIndex<T>(index: number, array: T[]) {
/**
* Reorders two objects in an array
* @param array
* @param fromIndex
* @param toIndex
*/
export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number) {
export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number): T[] {
if (fromIndex === toIndex) {
return array; // No change needed, return the original array
}
+4
View File
@@ -16,3 +16,7 @@ export function getPropertyFromPath<T extends object>(path: string, obj: T): unk
return result;
}
export function isObjectEmpty(obj: object): boolean {
return Object.keys(obj).length === 0;
}
+84 -72
View File
@@ -1,4 +1,4 @@
import type { OntimeDelay, OntimeEvent, OntimeRundown } from 'ontime-types';
import type { OntimeDelay, OntimeEntry, OntimeEvent, RundownEntries } from 'ontime-types';
import { SupportedEvent } from 'ontime-types';
import { getCueCandidate, getIncrement, sanitiseCue } from './cueUtils.js';
@@ -36,76 +36,83 @@ describe('getIncrement()', () => {
describe('getCueCandidate()', () => {
describe('in the beginning of the rundown', () => {
it('names cue as 1 if next event does not collide', () => {
const testRundown = [
{ id: '1', cue: '10', type: SupportedEvent.Event },
{ id: '2', cue: '11', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown);
const entries: RundownEntries = {
'1': { id: '1', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '11', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2']);
expect(cue).toBe('1');
});
it('creates decimal stem if next cue is 1', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '10', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown);
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2']);
expect(cue).toBe('0.1');
});
});
describe('in the middle of the rundown', () => {
it('names cue as an increment if next event has different stem (case of numbers)', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '10', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('2');
});
it('names cue as an increment if next event has different stem (case of letters)', () => {
const testRundown = [
{ id: '1', cue: 'Presenter', type: SupportedEvent.Event },
{
const entries: RundownEntries = {
'1': { id: '1', cue: 'Presenter', type: SupportedEvent.Event } as OntimeEvent,
'2': {
id: '2',
cue: 'Interval',
type: SupportedEvent.Event,
},
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
} as OntimeEntry,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('Presenter2');
});
it('creates decimal stem if next cue has same stem (case of numbers)', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '2', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '2', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('1.1');
});
it('creates decimal stem if next cue has same stem (case of letters)', () => {
const testRundown = [
{ id: '1', cue: 'Presenter1', type: SupportedEvent.Event },
{ id: '2', cue: 'Presenter2', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: 'Presenter1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: 'Presenter2', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('Presenter1.1');
});
});
describe('considers edge cases', () => {
it('previousEvent might not be a cue', () => {
const testRundown = [
{ id: '1', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
{ id: '2', type: SupportedEvent.Delay } as OntimeDelay,
];
const cue = getCueCandidate(testRundown, '2');
const entries: RundownEntries = {
'1': { id: '1', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
};
const cue = getCueCandidate(entries, ['1', '2'], '2');
expect(cue).toBe('11');
});
});
it('there might not be events before', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Delay } as OntimeDelay,
{ id: '2', type: SupportedEvent.Delay } as OntimeDelay,
];
const cue = getCueCandidate(testRundown, '2');
const entries: RundownEntries = {
'1': { id: '1', type: SupportedEvent.Delay } as OntimeDelay,
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
};
const cue = getCueCandidate(entries, ['1', '2'], '2');
expect(cue).toBe('1');
});
});
@@ -113,53 +120,58 @@ describe('getCueCandidate()', () => {
describe('findCueName() with mixed events', () => {
describe('in the beginning of the rundown', () => {
it('names cue as 1 if next event does not collide', () => {
const testRundown = [
{ id: '1', cue: '10', type: SupportedEvent.Event },
{ id: '2', cue: '11', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown);
const entries: RundownEntries = {
'1': { id: '1', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '11', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2']);
expect(cue).toBe('1');
});
it('creates decimal stem if next cue is 1', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '10', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown);
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2']);
expect(cue).toBe('0.1');
});
});
describe('in the middle of the rundown', () => {
it('names cue as an increment if next event has different stem (case of numbers)', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '10', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '10', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('2');
});
it('names cue as an increment if next event has different stem (case of letters)', () => {
const testRundown = [
{ id: '1', cue: 'Presenter', type: SupportedEvent.Event },
{ id: '2', cue: 'Interval', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: 'Presenter', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: 'Interval', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('Presenter2');
});
it('creates decimal stem if next cue has same stem (case of numbers)', () => {
const testRundown = [
{ id: '1', cue: '1', type: SupportedEvent.Event },
{ id: '2', cue: '2', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: '2', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('1.1');
});
it('creates decimal stem if next cue has same stem (case of letters)', () => {
const testRundown = [
{ id: '1', cue: 'Presenter1', type: SupportedEvent.Event },
{ id: '2', cue: 'Presenter2', type: SupportedEvent.Event },
] as OntimeRundown;
const cue = getCueCandidate(testRundown, '1');
const entries: RundownEntries = {
'1': { id: '1', cue: 'Presenter1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', cue: 'Presenter2', type: SupportedEvent.Event } as OntimeEvent,
};
const cue = getCueCandidate(entries, ['1', '2'], '1');
expect(cue).toBe('Presenter1.1');
});
});
+48 -47
View File
@@ -1,7 +1,7 @@
import type { OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import type { EntryId, OntimeEntry, RundownEntries } from 'ontime-types';
import { isOntimeEvent } from 'ontime-types';
import { getFirstEvent, getNextEvent, getPreviousEvent } from '../rundown-utils/rundownUtils.js';
import { getFirstEventNormal, getNextEventNormal, getPreviousEventNormal } from '../rundown-utils/rundownUtils.js';
import { isNumeric } from '../types/types.js';
// Zero or more non-digit characters at the beginning ((\D*)).
@@ -11,7 +11,6 @@ const regex = /^(\D*)(\d+)(\.\d+)?$/;
/**
* Finds if last characters in input are a number and increments
* @param input {string}
*/
export function getIncrement(input: string): string {
// Check if the input string contains a number at the end
@@ -41,55 +40,57 @@ export function getIncrement(input: string): string {
/**
* Gets suitable name for a new event cue
* @param rundown {OntimeRundown}
* @param insertAfterId {string}
*/
export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string): string {
function addAtTop() {
const firstEventCue = getFirstEvent(rundown).firstEvent?.cue;
export function getCueCandidate(entries: RundownEntries, order: EntryId[], insertAfterId?: string): string {
// we did not provide a element to go after, we attempt to go first so only need to check for a cue with value 1
if (insertAfterId === undefined || order.length === 0) {
return addAtTop();
}
if (isNumeric(firstEventCue)) {
return (Number(firstEventCue) / 10).toString();
// get the given event, or any before that
let previousEvent: OntimeEntry | null | undefined = entries[insertAfterId];
if (!isOntimeEvent(previousEvent)) {
previousEvent = getPreviousEventNormal(entries, order, insertAfterId).previousEvent;
if (!isOntimeEvent(previousEvent)) {
return addAtTop();
}
}
// the cue is based on the previous event cue
const cue = getIncrement(previousEvent.cue);
const { nextEvent } = getNextEventNormal(entries, order, insertAfterId);
// if increment is clashing with next, we add a decimal instead
if (cue !== nextEvent?.cue) {
return cue;
}
// there is a clash, bt the cue is a pure number
if (isNumeric(cue)) {
return incrementDecimal(previousEvent.cue);
}
/**
* at this point, we know the cue is not numeric
* but the increment failed, so we have a numeric ending
* eg. Presenter 1 .... Presenter 2 -> Presenter1.1
* eg. Presenter 1.1 .... Presenter 1.2 -> Presenter1.1.1
*/
return `${previousEvent.cue}.1`;
function incrementDecimal(cue: string) {
const n = Number(cue);
return (n + 0.1).toString();
}
function addAtTop() {
const firstEventCue = getFirstEventNormal(entries, order).firstEvent?.cue;
if (firstEventCue === '1') {
return '0.1';
}
return '1';
}
// we did not provide a element to go after, we attempt to go first so only need to check for a cue with value 1
if (typeof insertAfterId === 'undefined' || rundown.length === 0) {
return addAtTop();
}
const afterIndex = rundown.findIndex((event) => event.id === insertAfterId);
// we did not find the previous element, insert at top
if (afterIndex === -1) {
return addAtTop();
}
// get elements around
let previousEvent: OntimeRundownEntry | undefined | null | OntimeEvent = rundown.at(afterIndex);
if (!isOntimeEvent(previousEvent)) {
previousEvent = getPreviousEvent(rundown, insertAfterId).previousEvent as null | OntimeEvent;
}
let cue = '1';
const { nextEvent } = getNextEvent(rundown, insertAfterId);
// try and increment the cue
if (isOntimeEvent(previousEvent)) {
cue = getIncrement(previousEvent.cue);
}
// if increment is clashing with next, we add a decimal instead
if (cue === nextEvent?.cue) {
if (previousEvent === null) {
cue = '0.1';
} else {
cue = `${previousEvent.cue}.1`;
}
}
return cue;
}
export function sanitiseCue(cue: string) {
@@ -4,7 +4,7 @@ import { MILLIS_PER_HOUR } from './conversionUtils';
describe('checkIsNextDay', () => {
it('returns false if there is no previous event', () => {
const current = { timeStart: 0, dayOffset: 0 };
const previous = undefined;
const previous = null;
expect(checkIsNextDay(current, previous)).toBeFalsy();
});
@@ -7,7 +7,7 @@ import { dayInMs } from './conversionUtils.js';
*/
export function checkIsNextDay(
current: Pick<OntimeEvent, 'timeStart' | 'dayOffset'>,
previous?: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'>,
previous: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'> | null,
): boolean {
if (!previous) {
return false;
@@ -7,7 +7,7 @@ import { dayInMs } from './conversionUtils.js';
*/
export function getTimeFromPrevious(
current: Pick<OntimeEvent, 'timeStart' | 'dayOffset'>,
previous?: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'>,
previous: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'> | null,
): number {
// there is no previous event
if (!previous) {
@@ -4,7 +4,7 @@ import { isNewLatest } from './isNewLatest';
describe('isNewLatest', () => {
it('should be true if there is no previous', () => {
const current = { timeStart: 0, duration: MILLIS_PER_HOUR, dayOffset: 0 };
const previous = undefined;
const previous = null;
expect(isNewLatest(current, previous)).toBe(true);
});
+1 -1
View File
@@ -7,7 +7,7 @@ import { dayInMs } from './conversionUtils.js';
*/
export function isNewLatest(
currentEvent: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'>,
previousEvent?: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'>,
previousEvent: Pick<OntimeEvent, 'timeStart' | 'duration' | 'dayOffset'> | null,
) {
// true if there is no previous
if (!previousEvent) {
@@ -1,8 +1,7 @@
import type { NormalisedRundown, OntimeEvent, OntimeRundown } from 'ontime-types';
import type { OntimeBlock, OntimeDelay, OntimeEntry, OntimeEvent } from 'ontime-types';
import { SupportedEvent } from 'ontime-types';
import {
filterPlayable,
getLastEvent,
getLastNormal,
getNext,
@@ -15,36 +14,46 @@ import {
describe('getNext()', () => {
it('returns the next event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Event },
{ id: '3', type: SupportedEvent.Event },
];
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Event } as OntimeEvent,
'3': { id: '3', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3'],
};
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1');
const { nextEvent, nextIndex } = getNext(testRundown, '1');
expect(nextEvent?.id).toBe('2');
expect(nextIndex).toBe(1);
});
it('alows other event types', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
];
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1');
it('returns any type of OntimeEntry ', () => {
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
'3': { id: '3', type: SupportedEvent.Block } as OntimeBlock,
'4': { id: '4', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3', '4'],
};
const { nextEvent, nextIndex } = getNext(testRundown, '1');
expect(nextEvent?.id).toBe('2');
expect(nextIndex).toBe(1);
});
it('returns null if none found', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
];
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '3');
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Event } as OntimeEvent,
'3': { id: '3', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3'],
};
const { nextEvent, nextIndex } = getNext(testRundown, '3');
expect(nextEvent).toBe(null);
expect(nextIndex).toBe(null);
});
@@ -53,35 +62,37 @@ describe('getNext()', () => {
describe('getNextEvent()', () => {
it('returns the next event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Event },
{ id: '3', type: SupportedEvent.Event },
{ id: '1', type: SupportedEvent.Event } as OntimeEvent,
{ id: '2', type: SupportedEvent.Event } as OntimeEvent,
{ id: '3', type: SupportedEvent.Event } as OntimeEvent,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent?.id).toBe('2');
expect(nextIndex).toBe(1);
});
it('ignores other event types', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
{ id: '1', type: SupportedEvent.Event } as OntimeEvent,
{ id: '2', type: SupportedEvent.Delay } as OntimeDelay,
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
{ id: '4', type: SupportedEvent.Event } as OntimeEvent,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent?.id).toBe('4');
expect(nextIndex).toBe(3);
});
it('returns null if none found', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '1', type: SupportedEvent.Event } as OntimeEvent,
{ id: '2', type: SupportedEvent.Delay } as OntimeDelay,
{ id: '3', type: SupportedEvent.Block } as OntimeBlock,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent).toBe(null);
expect(nextIndex).toBe(null);
});
@@ -89,36 +100,46 @@ describe('getNextEvent()', () => {
describe('getPrevious()', () => {
it('returns the previous event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Event },
{ id: '3', type: SupportedEvent.Event },
];
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Event } as OntimeEvent,
'3': { id: '3', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3'],
};
const { entry, index } = getPrevious(testRundown as OntimeRundown, '3');
const { entry, index } = getPrevious(testRundown, '3');
expect(entry?.id).toBe('2');
expect(index).toBe(1);
});
it('allow other event types', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
];
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
'3': { id: '3', type: SupportedEvent.Block } as OntimeBlock,
'4': { id: '4', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3', '4'],
};
const { entry, index } = getPrevious(testRundown as OntimeRundown, '3');
const { entry, index } = getPrevious(testRundown, '3');
expect(entry?.id).toBe('2');
expect(index).toBe(1);
});
it('returns null if none found', () => {
const testRundown = [
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
];
const { entry, index } = getPrevious(testRundown as OntimeRundown, '2');
it('returns null if none found', () => {
const testRundown = {
entries: {
'2': { id: '2', type: SupportedEvent.Event } as OntimeEvent,
'3': { id: '3', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3'],
};
const { entry, index } = getPrevious(testRundown, '2');
expect(entry).toBe(null);
expect(index).toBe(null);
});
@@ -126,36 +147,47 @@ describe('getPrevious()', () => {
describe('getPreviousEvent()', () => {
it('returns the previous event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Event },
{ id: '3', type: SupportedEvent.Event },
];
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Event } as OntimeEvent,
'3': { id: '3', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3'],
};
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '3');
const { previousEvent, previousIndex } = getPreviousEvent(testRundown, '3');
expect(previousEvent?.id).toBe('2');
expect(previousIndex).toBe(1);
});
it('ignores other event types', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
];
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '4');
it('ignores other event types', () => {
const testRundown = {
entries: {
'1': { id: '1', type: SupportedEvent.Event } as OntimeEvent,
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
'3': { id: '3', type: SupportedEvent.Block } as OntimeBlock,
'4': { id: '4', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['1', '2', '3', '4'],
};
const { previousEvent, previousIndex } = getPreviousEvent(testRundown, '4');
expect(previousEvent?.id).toBe('1');
expect(previousIndex).toBe(0);
});
it('returns null if none found', () => {
const testRundown = [
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Block },
{ id: '4', type: SupportedEvent.Event },
];
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '2');
it('returns null if none found', () => {
const testRundown = {
entries: {
'2': { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
'3': { id: '3', type: SupportedEvent.Block } as OntimeBlock,
'4': { id: '4', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['2', '3', '4'],
};
const { previousEvent, previousIndex } = getPreviousEvent(testRundown, '2');
expect(previousEvent).toBe(null);
expect(previousIndex).toBe(null);
});
@@ -170,6 +202,7 @@ describe('swapEventData', () => {
timeEnd: 1,
duration: 1,
delay: 1,
revision: 3,
} as OntimeEvent;
const eventB = {
id: '2',
@@ -178,9 +211,10 @@ describe('swapEventData', () => {
timeEnd: 2,
duration: 2,
delay: 2,
revision: 7,
} as OntimeEvent;
const { newA, newB } = swapEventData(eventA, eventB);
const [newA, newB] = swapEventData(eventA, eventB);
expect(newA).toMatchObject({
id: '1',
@@ -189,6 +223,7 @@ describe('swapEventData', () => {
timeEnd: 1,
duration: 1,
delay: 1,
revision: 4,
});
expect(newB).toMatchObject({
id: '2',
@@ -197,121 +232,119 @@ describe('swapEventData', () => {
timeEnd: 2,
duration: 2,
delay: 2,
revision: 8,
});
});
});
describe('getLastEvent', () => {
it('returns the last event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEvent.Event },
{ id: '2', type: SupportedEvent.Delay },
{ id: '3', type: SupportedEvent.Event },
{ id: '4', type: SupportedEvent.Block },
const testRundown: OntimeEntry[] = [
{ id: '1', type: SupportedEvent.Event } as OntimeEvent,
{ id: '2', type: SupportedEvent.Delay } as OntimeDelay,
{ id: '3', type: SupportedEvent.Event } as OntimeEvent,
{ id: '4', type: SupportedEvent.Block } as OntimeBlock,
];
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
const { lastEvent } = getLastEvent(testRundown);
expect(lastEvent?.id).toBe('3');
});
it('handles rundowns with a single event', () => {
const testRundown = [{ id: '1', type: SupportedEvent.Event }];
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
it('handles rundowns with a single event', () => {
const testRundown: OntimeEntry[] = [{ id: '1', type: SupportedEvent.Event } as OntimeEvent];
const { lastEvent } = getLastEvent(testRundown);
expect(lastEvent?.id).toBe('1');
});
describe('getLastNormal', () => {
it('returns the last entry', () => {
const testRundown = {
4: { id: '4', type: SupportedEvent.Block },
1: { id: '1', type: SupportedEvent.Event },
3: { id: '3', type: SupportedEvent.Event },
2: { id: '2', type: SupportedEvent.Delay },
const entries = {
4: { id: '4', type: SupportedEvent.Block } as OntimeBlock,
1: { id: '1', type: SupportedEvent.Event } as OntimeEvent,
3: { id: '3', type: SupportedEvent.Event } as OntimeEvent,
2: { id: '2', type: SupportedEvent.Delay } as OntimeDelay,
};
const order = ['1', '2', '3', '4'];
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
const lastEntry = getLastNormal(entries, order);
expect(lastEntry?.id).toBe('4');
});
it('handles rundowns with a single event', () => {
const testRundown = [{ id: '1', type: SupportedEvent.Event }];
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
it('handles rundowns with a single event', () => {
const entries = {
1: { id: '1', type: SupportedEvent.Event } as OntimeEvent,
};
const lastEvent = getLastNormal(entries, ['1']);
expect(lastEvent?.id).toBe('1');
});
it('handles empty order', () => {
const testRundown = {
4: { id: '4', type: SupportedEvent.Block },
1: { id: '1', type: SupportedEvent.Event },
3: { id: '3', type: SupportedEvent.Event },
2: { id: '2', type: SupportedEvent.Delay },
1: { id: '1', type: SupportedEvent.Event } as OntimeEvent,
2: { id: '2', type: SupportedEvent.Event } as OntimeEvent,
};
const order: string[] = [];
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
const lastEntry = getLastNormal(testRundown, []);
expect(lastEntry).toBe(null);
});
it('handles empty rundown', () => {
const testRundown = {};
const order = ['1', '2', '3', '4'];
const lastEntry = getLastNormal(testRundown as unknown as NormalisedRundown, order);
const lastEntry = getLastNormal({}, ['1', '2', '3', '4']);
expect(lastEntry).toBe(null);
});
});
describe('relevantBlock', () => {
const testRundown = [
{ id: 'a', type: SupportedEvent.Event },
{ id: 'b', type: SupportedEvent.Event },
{ id: 'c', type: SupportedEvent.Event },
{ id: 'd', type: SupportedEvent.Delay },
{ id: 'e', type: SupportedEvent.Block },
{ id: 'f', type: SupportedEvent.Event },
{ id: 'g', type: SupportedEvent.Block },
{ id: 'h', type: SupportedEvent.Event },
];
describe('getPreviousBlock()', () => {
const testRundown = {
entries: {
a: { id: 'a', type: SupportedEvent.Event } as OntimeEvent,
b: { id: 'b', type: SupportedEvent.Event } as OntimeEvent,
c: { id: 'c', type: SupportedEvent.Event } as OntimeEvent,
d: { id: 'd', type: SupportedEvent.Delay } as OntimeDelay,
e: { id: 'e', type: SupportedEvent.Block } as OntimeBlock,
f: { id: 'f', type: SupportedEvent.Event } as OntimeEvent,
g: { id: 'g', type: SupportedEvent.Block } as OntimeBlock,
h: { id: 'h', type: SupportedEvent.Event } as OntimeEvent,
},
order: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
};
it('returns the relevant block', () => {
const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'h');
expect(block?.id).toBe('g');
test.each([
['h', 'g'],
['f', 'e'],
])('returns the relevant block', (id, expected) => {
const block = getPreviousBlock(testRundown, id);
expect(block?.id).toBe(expected);
});
it('returns the relevant block', () => {
const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'f');
expect(block?.id).toBe('e');
it('returns null if there is no parent block relevant block', () => {
const block = getPreviousBlock(testRundown, 'a');
expect(block).toBe(null);
});
it('returns the relevant block', () => {
const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'a');
expect(block).toBeNull();
});
it('also works on index 0', () => {
testRundown.unshift({ id: '0', type: SupportedEvent.Block });
const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'a');
testRundown.order.unshift('0');
// @ts-expect-error -- we are adding an event to the rundown
testRundown.entries['0'] = { id: '0', type: SupportedEvent.Block } as OntimeBlock;
const block = getPreviousBlock(testRundown, 'a');
expect(block?.id).toBe('0');
});
});
describe('filterPlayable()', () => {
test('should return an array with only playable events', () => {
const eventA = { id: 'a', type: SupportedEvent.Event } as OntimeEvent;
const eventB = { id: 'b', skip: true, type: SupportedEvent.Event } as OntimeEvent;
const testRundown = [
eventA,
eventB,
{ id: 'c', type: SupportedEvent.Delay },
{ id: 'd', type: SupportedEvent.Block },
];
const result = filterPlayable(testRundown as unknown as OntimeRundown);
expect(result).toMatchObject([eventA]);
it('returns the parent block if nested event', () => {
const testRundown = {
entries: {
1: { id: '1', type: SupportedEvent.Event } as OntimeEvent,
block: { id: 'block', type: SupportedEvent.Block, events: ['21', '22', '23'] } as OntimeBlock,
21: { id: '21', type: SupportedEvent.Event, currentBlock: 'block' } as OntimeEvent,
22: { id: '22', type: SupportedEvent.Event, currentBlock: 'block' } as OntimeEvent,
23: { id: '23', type: SupportedEvent.Event, currentBlock: 'block' } as OntimeEvent,
},
order: ['1', 'block'],
};
const block = getPreviousBlock(testRundown, '21');
expect(block?.id).toBe('block');
});
});
});
@@ -1,37 +1,31 @@
import type {
NormalisedRundown,
EntryId,
OntimeBlock,
OntimeEntry,
OntimeEvent,
OntimeRundown,
OntimeRundownEntry,
PlayableEvent,
Rundown,
RundownEntries,
} from 'ontime-types';
import { isOntimeBlock, isOntimeEvent, isPlayableEvent } from 'ontime-types';
type IndexAndEntry = { entry: OntimeRundownEntry | null; index: number | null };
/**
* Gets first event in rundown, if it exists
*/
export function getFirst(rundown: OntimeRundown) {
return rundown.length ? rundown[0] : null;
}
type IndexAndEntry = { entry: OntimeEntry | null; index: number | null };
/**
* Gets first event in a normalised rundown, if it exists
* @param rundown
* @param order
* @returns
*/
export function getFirstNormal(rundown: NormalisedRundown, order: string[]) {
const firstId = order[0];
return rundown[firstId] ?? null;
export function getFirstNormal(entries: RundownEntries, order: EntryId[]): OntimeEntry | null {
if (!order.length) {
return null;
}
const eventId = order[0];
return entries[eventId] ?? null;
}
/**
* Gets first scheduled event in rundown, if it exists
*/
export function getFirstEvent(rundown: OntimeRundown): {
export function getFirstEvent(rundown: OntimeEntry[]): {
firstEvent: PlayableEvent | null;
firstIndex: number | null;
} {
@@ -48,8 +42,8 @@ export function getFirstEvent(rundown: OntimeRundown): {
* Gets first scheduled event in a normalised rundown, if it exists
*/
export function getFirstEventNormal(
rundown: NormalisedRundown,
order: string[],
rundown: RundownEntries,
order: EntryId[],
): {
firstEvent: PlayableEvent | null;
firstIndex: number | null;
@@ -67,18 +61,18 @@ export function getFirstEventNormal(
/**
* Gets last event in a normalised rundown, if it exists
*/
export function getLastNormal(rundown: NormalisedRundown, order: string[]): OntimeRundownEntry | null {
export function getLastNormal(entries: RundownEntries, order: EntryId[]): OntimeEntry | null {
const lastId = order.at(-1);
if (lastId === undefined) {
return null;
}
return rundown[lastId] ?? null;
return entries[lastId] ?? null;
}
/**
* Gets last scheduled event in rundown, if it exists
*/
export function getLastEvent(rundown: OntimeRundown): {
export function getLastEvent(rundown: OntimeEntry[]): {
lastEvent: PlayableEvent | null;
lastIndex: number | null;
} {
@@ -87,7 +81,7 @@ export function getLastEvent(rundown: OntimeRundown): {
}
for (let i = rundown.length - 1; i >= 0; i--) {
const lastEvent = rundown.at(i);
const lastEvent = rundown[i];
if (isOntimeEvent(lastEvent) && isPlayableEvent(lastEvent)) {
return { lastEvent, lastIndex: i };
}
@@ -99,7 +93,7 @@ export function getLastEvent(rundown: OntimeRundown): {
* Gets last scheduled event in a normalised rundown, if it exists
*/
export function getLastEventNormal(
rundown: NormalisedRundown,
rundown: RundownEntries,
order: string[],
): {
lastEvent: OntimeEvent | null;
@@ -123,13 +117,14 @@ export function getLastEventNormal(
* Gets next entry in rundown, if it exists
*/
export function getNext(
rundown: OntimeRundown,
rundown: Pick<Rundown, 'entries' | 'order'>,
currentId: string,
): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } {
const index = rundown.findIndex((event) => event.id === currentId);
if (index !== -1 && index + 1 < rundown.length) {
): { nextEvent: OntimeEntry | null; nextIndex: number | null } {
const index = rundown.order.findIndex((entryId) => entryId === currentId);
if (index !== -1 && index + 1 < rundown.order.length) {
const nextIndex = index + 1;
const nextEvent = rundown[nextIndex];
const nextId = rundown.order[nextIndex];
const nextEvent = rundown.entries[nextId];
return { nextEvent, nextIndex };
} else {
return { nextEvent: null, nextIndex: null };
@@ -139,7 +134,7 @@ export function getNext(
/**
* Gets next entry in rundown, if it exists
*/
export function getNextNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
export function getNextNormal(rundown: RundownEntries, order: string[], currentId: string): IndexAndEntry {
const currentIndex = order.findIndex((id) => id === currentId);
if (currentIndex !== -1 && currentIndex + 1 < order.length) {
const index = currentIndex + 1;
@@ -155,10 +150,10 @@ export function getNextNormal(rundown: NormalisedRundown, order: string[], curre
* Gets next scheduled event in rundown, if it exists
*/
export function getNextEvent(
rundown: OntimeRundown,
rundown: OntimeEntry[],
currentId: string,
): { nextEvent: OntimeEvent | null; nextIndex: number | null } {
const index = rundown.findIndex((event) => event.id === currentId);
const index = rundown.findIndex((entry) => entry.id === currentId);
if (index < 0) {
return { nextEvent: null, nextIndex: null };
}
@@ -176,18 +171,18 @@ export function getNextEvent(
* Gets next scheduled event in a normalised rundown, if it exists
*/
export function getNextEventNormal(
rundown: NormalisedRundown,
order: string[],
entries: RundownEntries,
order: EntryId[],
currentId: string,
): { nextEvent: OntimeEvent | null; nextIndex: number | null } {
const index = order.findIndex((id) => id === currentId);
const index = order.findIndex((entryId) => entryId === currentId);
if (index < 0) {
return { nextEvent: null, nextIndex: null };
}
for (let i = index + 1; i < order.length; i++) {
const nextId = order[i];
const nextEvent = rundown[nextId];
const nextEvent = entries[nextId];
if (isOntimeEvent(nextEvent)) {
return { nextEvent, nextIndex: i };
}
@@ -198,11 +193,13 @@ export function getNextEventNormal(
/**
* Gets previous entry in rundown, if it exists
*/
export function getPrevious(rundown: OntimeRundown, currentId: string): IndexAndEntry {
const currentIndex = rundown.findIndex((event) => event.id === currentId);
if (currentIndex !== -1 && currentIndex - 1 >= 0) {
export function getPrevious(rundown: Pick<Rundown, 'entries' | 'order'>, currentId: string): IndexAndEntry {
const currentIndex = rundown.order.findIndex((entryId) => entryId === currentId);
if (currentIndex > 1) {
const index = currentIndex - 1;
const entry = rundown[index];
const previousId = rundown.order[index];
const entry = rundown.entries[previousId];
return { entry, index };
} else {
return { entry: null, index: null };
@@ -210,14 +207,15 @@ export function getPrevious(rundown: OntimeRundown, currentId: string): IndexAnd
}
/**
* Gets previous entry in a nornalised rundown, if it exists
* Gets previous entry in a normalised rundown, if it exists
*/
export function getPreviousNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
export function getPreviousNormal(entries: RundownEntries, order: string[], currentId: string): IndexAndEntry {
const currentIndex = order.findIndex((id) => id === currentId);
if (currentIndex !== -1 && currentIndex - 1 >= 0) {
const index = currentIndex - 1;
const previousId = order[index];
const entry = rundown[previousId];
const entry = entries[previousId];
return { entry, index };
} else {
return { entry: null, index: null };
@@ -228,15 +226,16 @@ export function getPreviousNormal(rundown: NormalisedRundown, order: string[], c
* Gets previous scheduled event in rundown, if it exists
*/
export function getPreviousEvent(
rundown: OntimeRundown,
rundown: Pick<Rundown, 'entries' | 'order'>,
currentId: string,
): { previousEvent: OntimeEvent | null; previousIndex: number | null } {
const index = rundown.findIndex((event) => event.id === currentId);
const index = rundown.order.findIndex((entryId) => entryId === currentId);
if (index < 0) {
return { previousEvent: null, previousIndex: null };
}
for (let i = index - 1; i >= 0; i--) {
const previousEvent = rundown[i];
const previousId = rundown.order[i];
const previousEvent = rundown.entries[previousId];
if (isOntimeEvent(previousEvent)) {
return { previousEvent, previousIndex: i };
}
@@ -246,23 +245,19 @@ export function getPreviousEvent(
/**
* Gets previous scheduled event in a normalised rundown, if it exists
* @param rundown
* @param order
* @param {string} currentId
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
*/
export function getPreviousEventNormal(
rundown: NormalisedRundown,
order: string[],
entries: RundownEntries,
order: EntryId[],
currentId: string,
): { previousEvent: OntimeEvent | null; previousIndex: number | null } {
const index = order.findIndex((id) => id === currentId);
const index = order.findIndex((entryId) => entryId === currentId);
if (index < 0) {
return { previousEvent: null, previousIndex: null };
}
for (let i = index - 1; i >= 0; i--) {
const previousId = order[i];
const previousEvent = rundown[previousId];
const previousEvent = entries[previousId];
if (isOntimeEvent(previousEvent)) {
return { previousEvent, previousIndex: i };
}
@@ -273,7 +268,7 @@ export function getPreviousEventNormal(
/**
* @description swaps two OntimeEvents in the rundown
*/
export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): { newA: OntimeEvent; newB: OntimeEvent } => {
export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): [newA: OntimeEvent, newB: OntimeEvent] => {
const newA = {
...eventB,
// events keep the ID
@@ -304,17 +299,17 @@ export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): { newA:
dayOffset: eventB.dayOffset,
};
return { newA, newB };
return [newA, newB];
};
export function getEventWithId(rundown: OntimeRundown, id: string): OntimeRundownEntry | undefined {
export function getEventWithId(rundown: OntimeEntry[], id: string): OntimeEntry | undefined {
return rundown.find((event) => event.id === id);
}
/**
* Gets relevant block element for a given ID
*/
export function getPreviousBlockNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
export function getPreviousBlockNormal(rundown: RundownEntries, order: string[], currentId: string): IndexAndEntry {
let foundCurrentEvent = false;
// Iterate backwards through the rundown to find the current event
for (let index = order.length - 1; index >= 0; index--) {
@@ -337,7 +332,7 @@ export function getPreviousBlockNormal(rundown: NormalisedRundown, order: string
/**
* Gets next block element for a given ID
*/
export function getNextBlockNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
export function getNextBlockNormal(rundown: RundownEntries, order: string[], currentId: string): IndexAndEntry {
let foundCurrentEvent = false;
// Iterate backwards through the rundown to find the current event
for (let index = 0; index < order.length; index++) {
@@ -360,11 +355,19 @@ export function getNextBlockNormal(rundown: NormalisedRundown, order: string[],
/**
* Gets relevant block element for a given ID
*/
export function getPreviousBlock(rundown: OntimeRundown, currentId: string): OntimeBlock | null {
export function getPreviousBlock(rundown: Pick<Rundown, 'entries' | 'order'>, currentId: EntryId): OntimeBlock | null {
const currentEvent = rundown.entries[currentId];
// check if event is inside a block
if (isOntimeEvent(currentEvent) && currentEvent.currentBlock) {
return rundown.entries[currentEvent.currentBlock] as OntimeBlock;
}
let foundCurrentEvent = false;
// Iterate backwards through the rundown to find the current event
for (let i = rundown.length - 1; i >= 0; i--) {
const entry = rundown[i];
for (let i = rundown.order.length - 1; i >= 0; i--) {
const entryId = rundown.order[i];
const entry = rundown.entries[entryId];
if (!foundCurrentEvent && entry.id === currentId) {
// set the flag when the current event is found
foundCurrentEvent = true;
@@ -375,20 +378,6 @@ export function getPreviousBlock(rundown: OntimeRundown, currentId: string): Ont
return entry;
}
}
// no blocks exist before current event
// no blocks exist before null event
return null;
}
/**
* filters a rundown to timed events
*/
export function filterPlayable(rundown: OntimeRundown): PlayableEvent[] {
return rundown.filter((event) => isOntimeEvent(event) && !event.skip) as PlayableEvent[];
}
/**
* filters a rundown to events that can be played
*/
export function filterTimedEvents(rundown: OntimeRundown): OntimeEvent[] {
return rundown.filter((event) => isOntimeEvent(event)) as OntimeEvent[];
}