diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index ada944e9e4..d99ea84af6 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -115,6 +115,7 @@ export const setAuxTimer = { export const useCuesheet = () => { const featureSelector = (state: RuntimeStore) => ({ playback: state.timer.playback, + currentBlockId: state.currentBlock.block?.id ?? null, selectedEventId: state.eventNow?.id ?? null, selectedEventIndex: state.runtime.selectedEventIndex, numEvents: state.runtime.numEvents, diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index dd5a5667f0..8c20128a7d 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -38,6 +38,10 @@ export const runtimeStorePlaceholder: RuntimeStore = { actualStart: null, expectedEnd: null, }, + currentBlock: { + block: null, + startedAt: null, + }, eventNow: null, eventNext: null, publicEventNow: null, diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index b8b79a13f9..e05e2fc6a8 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -150,6 +150,11 @@ export const connectSocket = () => { updateDevTools({ eventNow: payload }); break; } + case 'ontime-currentBlock': { + patchRuntime('currentBlock', payload); + updateDevTools({ currentBlock: payload }); + break; + } case 'ontime-publicEventNow': { patchRuntime('publicEventNow', payload); updateDevTools({ publicEventNow: payload }); diff --git a/apps/client/src/features/cuesheet/Cuesheet.tsx b/apps/client/src/features/cuesheet/Cuesheet.tsx index e3e641d038..3df5eb2412 100644 --- a/apps/client/src/features/cuesheet/Cuesheet.tsx +++ b/apps/client/src/features/cuesheet/Cuesheet.tsx @@ -21,11 +21,11 @@ interface CuesheetProps { columns: ColumnDef[]; handleUpdate: (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: unknown) => void; selectedId: string | null; + currentBlockId: string | null; } -export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) { +export default function Cuesheet({ data, columns, handleUpdate, selectedId, currentBlockId }: CuesheetProps) { const { followSelected, showSettings, showDelayBlock, showPrevious, showIndexColumn } = useCuesheetSettings(); - const { columnVisibility, columnOrder, @@ -114,11 +114,16 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu } if (isOntimeBlock(row.original)) { + if (isPast && !showPrevious && key !== currentBlockId) { + return null; + } return ; } if (isOntimeDelay(row.original)) { + if (isPast && !showPrevious) { + return null; + } const delayVal = row.original.duration; - if (!showDelayBlock || delayVal === 0) { return null; } @@ -128,9 +133,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu if (isOntimeEvent(row.original)) { eventIndex++; const isSelected = key === selectedId; - if (isSelected) { - isPast = false; - } if (isPast && !showPrevious) { return null; diff --git a/apps/client/src/features/cuesheet/CuesheetWrapper.tsx b/apps/client/src/features/cuesheet/CuesheetWrapper.tsx index 733aeb40f2..efb71ae81a 100644 --- a/apps/client/src/features/cuesheet/CuesheetWrapper.tsx +++ b/apps/client/src/features/cuesheet/CuesheetWrapper.tsx @@ -107,6 +107,7 @@ export default function CuesheetWrapper() { columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} + currentBlockId={featureData.currentBlockId} /> ); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index bf0f383dd2..feb8cb53c3 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -179,6 +179,10 @@ export const startServer = async ( message: messageService.getState(), runtime: state.runtime, eventNow: state.eventNow, + currentBlock: { + block: null, + startedAt: null, + }, publicEventNow: state.publicEventNow, eventNext: state.eventNext, publicEventNext: state.publicEventNext, diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index dc6455e006..ff1c16a2d6 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -1,4 +1,4 @@ -import { OntimeEvent } from 'ontime-types'; +import { OntimeRundown } from 'ontime-types'; import * as runtimeState from '../stores/runtimeState.js'; import type { UpdateResult } from '../stores/runtimeState.js'; @@ -106,7 +106,7 @@ export class TimerService { * Loads roll information into timer service * @param {OntimeEvent[]} rundown -- list of events to run */ - roll(rundown: OntimeEvent[]) { + roll(rundown: OntimeRundown) { runtimeState.roll(rundown); } diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index 3167206322..8133e0443a 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -25,13 +25,14 @@ import { eventStore } from '../../stores/EventStore.js'; type PatchWithId = (Partial | Partial | Partial) & { id: string }; -type CompleteEntry = T extends Partial - ? OntimeEvent - : T extends Partial - ? OntimeDelay - : T extends Partial - ? OntimeBlock - : never; +type CompleteEntry = + T extends Partial + ? OntimeEvent + : T extends Partial + ? OntimeDelay + : T extends Partial + ? OntimeBlock + : never; function generateEvent | Partial | Partial>( eventData: T, @@ -240,7 +241,7 @@ function notifyChanges(options: { timer?: boolean | string[]; external?: boolean // notify timer service of changed events // timer can be true or an array of changed IDs const affected = Array.isArray(options.timer) ? options.timer : undefined; - runtimeService.maybeUpdate(playableEvents, affected); + runtimeService.maybeUpdate(affected); } } diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 5936ffcd3b..5691a3e172 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -9,7 +9,7 @@ import { TimerLifeCycle, TimerPhase, } from 'ontime-types'; -import { millisToString, validatePlayback } from 'ontime-utils'; +import { filterPlayable, millisToString, validatePlayback } from 'ontime-utils'; import { deepEqual } from 'fast-equals'; @@ -28,6 +28,7 @@ import { getNextEventWithCue, getEventWithId, getPlayableEvents, + getRundown, } from '../rundown-service/rundownUtils.js'; import { skippedOutOfEvent } from '../timerUtils.js'; import { integrationService } from '../integration-service/IntegrationService.js'; @@ -95,7 +96,7 @@ class RuntimeService { } // we dont call this.roll because we need to bypass the checks - const rundown = getPlayableEvents(); + const rundown = getRundown(); // TODO: by not calling roll, we dont get the events this.eventTimer.roll(rundown); } @@ -217,7 +218,7 @@ class RuntimeService { * Called when the underlying data has changed, * we check if the change affects the runtime */ - maybeUpdate(playableEvents: OntimeEvent[], affectedIds?: string[]) { + maybeUpdate(affectedIds?: string[]) { const state = runtimeState.getState(); const hasLoadedElements = state.eventNow !== null || state.eventNext !== null; if (!hasLoadedElements) { @@ -245,7 +246,8 @@ class RuntimeService { if (onlyChangedNow) { runtimeState.reload(eventNow); } else { - runtimeState.reloadAll(eventNow, playableEvents); + const rundown = getRundown(); + runtimeState.reloadAll(eventNow, rundown); } return; } @@ -253,7 +255,8 @@ class RuntimeService { // Maybe the event will become the next isNext = this.isNewNext(); if (isNext) { - runtimeState.loadNext(playableEvents); + const rundown = getRundown(); + runtimeState.loadNext(rundown); } } @@ -269,8 +272,8 @@ class RuntimeService { return false; } - const timedEvents = getPlayableEvents(); - const success = runtimeState.load(event, timedEvents); + const rundown = getRundown(); + const success = runtimeState.load(event, rundown); if (success) { logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`); @@ -513,13 +516,14 @@ class RuntimeService { return; } - const playableEvents = getPlayableEvents(); + const rundown = getRundown(); + const playableEvents = filterPlayable(rundown); if (playableEvents.length === 0) { logger.warning(LogOrigin.Server, 'Roll: no events found'); return; } - this.eventTimer.roll(playableEvents); + this.eventTimer.roll(rundown); const state = runtimeState.getState(); const newState = state.timer.playback; @@ -543,14 +547,14 @@ class RuntimeService { } // the db would have to change for the event not to exist - // we do not kow the reason for the crash, so we check anyway + // we do not know the reason for the crash, so we check anyway const event = getEventWithId(selectedEventId); if (!event || !isOntimeEvent(event)) { return; } - const timedEvents = getPlayableEvents(); - runtimeState.resume(restorePoint, event, timedEvents); + const rundown = getRundown(); + runtimeState.resume(restorePoint, event, rundown); logger.info(LogOrigin.Playback, 'Resuming playback'); } @@ -620,6 +624,11 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert updateEventIfChanged('eventNext', state); updateEventIfChanged('publicEventNext', state); + if (!deepEqual(RuntimeService?.previousState.currentBlock, state.currentBlock)) { + eventStore.set('currentBlock', state.currentBlock); + RuntimeService.previousState.currentBlock = { ...state.currentBlock }; + } + if (shouldUpdateClock) { RuntimeService.previousClockUpdate = state.clock; eventStore.set('clock', state.clock); diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 3ad68df87f..2f78bee39b 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -127,10 +127,14 @@ type RollTimers = { /** * Finds loading information given a current rundown and time - * @param {OntimeEvent[]} rundown - List of playable events + * @param {OntimeEvent[]} playableEvents - List of playable events * @param {number} timeNow - time now in ms */ -export const getRollTimers = (rundown: OntimeEvent[], timeNow: number, currentIndex?: number | null): RollTimers => { +export const getRollTimers = ( + playableEvents: OntimeEvent[], + timeNow: number, + currentIndex?: number | null, +): RollTimers => { let nowIndex: MaybeNumber = null; // index of event now let nowId: MaybeString = null; // id of event now let publicIndex: MaybeNumber = null; // index of public event now @@ -140,8 +144,8 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number, currentIn let publicTimeToNext: MaybeNumber = null; // counter: time for next public event const hasLoaded = currentIndex !== null; - const canFilter = hasLoaded && currentIndex === rundown.length - 1; - const filteredRundown = canFilter ? rundown.slice(currentIndex) : rundown; + const canFilter = hasLoaded && currentIndex === playableEvents.length - 1; + const filteredRundown = canFilter ? playableEvents.slice(currentIndex) : playableEvents; const lastEvent = filteredRundown.at(-1); const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd); diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index d5b76d5b3a..17481a0167 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -98,6 +98,7 @@ describe('mutation on runtimeState', () => { expect(newState.eventNow?.id).toBe(mockEvent.id); expect(newState.timer.playback).toBe(Playback.Armed); expect(newState.clock).not.toBe(666); + expect(newState.currentBlock.block).toBeNull(); // 2. Start event let success = start(); @@ -171,6 +172,7 @@ describe('mutation on runtimeState', () => { expect(newState.runtime.actualStart).toBeNull(); expect(newState.runtime.plannedStart).toBe(0); expect(newState.runtime.plannedEnd).toBe(1500); + expect(newState.currentBlock.block).toBeNull(); // 2. Start event start(); @@ -202,6 +204,7 @@ describe('mutation on runtimeState', () => { expect(newState.runtime.offset).toBe(delayBefore); // finish is the difference between the runtime and the schedule expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset); + expect(newState.currentBlock.block).toBeNull(); // 4. Add time addTime(10); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 0bff88afa5..1430b7ae3b 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -1,5 +1,14 @@ -import { MaybeNumber, OntimeEvent, Playback, Runtime, TimerPhase, TimerState } from 'ontime-types'; -import { calculateDuration, dayInMs } from 'ontime-utils'; +import { + CurrentBlockState, + MaybeNumber, + OntimeEvent, + OntimeRundown, + Playback, + Runtime, + TimerPhase, + TimerState, +} from 'ontime-types'; +import { calculateDuration, dayInMs, filterPlayable, getRelevantBlock } from 'ontime-utils'; import { clock } from '../services/Clock.js'; import { RestorePoint } from '../services/RestoreService.js'; @@ -41,6 +50,7 @@ const initialTimer: TimerState = { export type RuntimeState = { clock: number; // realtime clock eventNow: OntimeEvent | null; + currentBlock: CurrentBlockState; publicEventNow: OntimeEvent | null; eventNext: OntimeEvent | null; publicEventNext: OntimeEvent | null; @@ -52,10 +62,15 @@ export type RuntimeState = { totalDelay: number; // this value comes from rundown service pausedAt: MaybeNumber; }; + _prevCurrentBlock: CurrentBlockState; }; const runtimeState: RuntimeState = { clock: clock.timeNow(), + currentBlock: { + block: null, + startedAt: null, + }, eventNow: null, publicEventNow: null, eventNext: null, @@ -67,6 +82,10 @@ const runtimeState: RuntimeState = { totalDelay: 0, pausedAt: null, }, + _prevCurrentBlock: { + block: null, + startedAt: null, + }, }; export function getState(): Readonly { @@ -77,6 +96,11 @@ export function clear() { runtimeState.eventNow = null; runtimeState.publicEventNow = null; runtimeState.eventNext = null; + + runtimeState._prevCurrentBlock = { ...runtimeState.currentBlock }; + runtimeState.currentBlock.block = null; + runtimeState.currentBlock.startedAt = null; + runtimeState.publicEventNext = null; runtimeState.runtime.offset = 0; @@ -129,12 +153,13 @@ export function updateRundownData(rundownData: RundownData) { /** * Loads a given event into state * @param event - * @param rundown - * @param initialData + * @param {OntimeEvent[]} playableEvents list of events availebe for playback + * @param {OntimeRundown} rundown the full rundown + * @param initialData potential data from restore point */ export function load( event: OntimeEvent, - rundown: OntimeEvent[], + rundown: OntimeRundown, initialData?: Partial, ): boolean { clear(); @@ -165,8 +190,14 @@ export function load( return event.id === runtimeState.eventNow?.id; } -export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) { +export function loadNow(event: OntimeEvent, rundown: OntimeRundown) { runtimeState.eventNow = event; + runtimeState.currentBlock.block = getRelevantBlock(rundown, event.id); + + //if we are still in the same block keep the startedAt time + if (runtimeState._prevCurrentBlock.block?.id === runtimeState.currentBlock.block?.id) { + runtimeState.currentBlock.startedAt = runtimeState._prevCurrentBlock.startedAt; + } // check if current is also public if (event.isPublic) { @@ -180,6 +211,8 @@ export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) { return; } + const playableEvents = filterPlayable(rundown); + // iterate backwards to find it for (let i = runtimeState.runtime.selectedEventIndex; i >= 0; i--) { if (playableEvents[i].isPublic) { @@ -190,7 +223,7 @@ export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) { } } -export function loadNext(playableEvents: OntimeEvent[]) { +export function loadNext(rundown: OntimeRundown) { // assume there are no next events runtimeState.eventNext = null; runtimeState.publicEventNext = null; @@ -199,6 +232,7 @@ export function loadNext(playableEvents: OntimeEvent[]) { return; } + const playableEvents = filterPlayable(rundown); const numEvents = playableEvents.length; if (runtimeState.runtime.selectedEventIndex < numEvents - 1) { @@ -224,7 +258,14 @@ export function loadNext(playableEvents: OntimeEvent[]) { } } -export function resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeEvent[]) { +/** + * Resume from restore point + * @param restorePoint + * @param event + * @param playableEvents list of events availebe for playback + * @param rundown the full rundown + */ +export function resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeRundown) { load(event, rundown, restorePoint); } @@ -255,6 +296,8 @@ export function reload(event?: OntimeEvent) { runtimeState.timer.addedTime = 0; runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState); + + runtimeState.currentBlock.startedAt = null; return runtimeState.eventNow.id; } @@ -263,10 +306,11 @@ export function reload(event?: OntimeEvent) { * without interrupting timer * @param eventNow * @param playableEvents + * @param rundown */ -export function reloadAll(eventNow: OntimeEvent, playableEvents: OntimeEvent[]) { - loadNow(eventNow, playableEvents); - loadNext(playableEvents); +export function reloadAll(eventNow: OntimeEvent, rundown: OntimeRundown) { + loadNow(eventNow, rundown); + loadNext(rundown); reload(eventNow); } @@ -291,6 +335,11 @@ export function start(state: RuntimeState = runtimeState): boolean { state.timer.startedAt = state.clock; } + if (state.currentBlock.startedAt === null) { + console.log('currentBlock.startedAt is null, setting new start'); + state.currentBlock.startedAt = state.clock; + } + state.timer.playback = Playback.Play; state.timer.expectedFinish = getExpectedFinish(state); state.timer.elapsed = 0; @@ -427,12 +476,14 @@ export function update(): UpdateResult { } } -export function roll(rundown: OntimeEvent[]) { +export function roll(rundown: OntimeRundown) { const selectedEventIndex = runtimeState.runtime.selectedEventIndex; + const playableEvents = filterPlayable(rundown); + clear(); - runtimeState.runtime.numEvents = rundown.length; + runtimeState.runtime.numEvents = playableEvents.length; - const { nextEvent, currentEvent } = getRollTimers(rundown, runtimeState.clock, selectedEventIndex); + const { nextEvent, currentEvent } = getRollTimers(playableEvents, runtimeState.clock, selectedEventIndex); if (currentEvent) { // there is something running, load diff --git a/packages/types/src/definitions/runtime/CurrentBlockState.type.ts b/packages/types/src/definitions/runtime/CurrentBlockState.type.ts new file mode 100644 index 0000000000..4e6fd80e4a --- /dev/null +++ b/packages/types/src/definitions/runtime/CurrentBlockState.type.ts @@ -0,0 +1,7 @@ +import type { MaybeNumber } from '../../utils/utils.type.js'; +import type { OntimeBlock } from '../core/OntimeEvent.type.js'; + +export type CurrentBlockState = { + block: OntimeBlock | null; + startedAt: MaybeNumber; +}; diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts index 123d180f15..dfd2c8f9b9 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts @@ -1,5 +1,6 @@ import type { OntimeEvent } from '../core/OntimeEvent.type.js'; import type { SimpleTimerState } from './AuxTimer.type.js'; +import type { CurrentBlockState } from './CurrentBlockState.type.js'; import type { MessageState } from './MessageControl.type.js'; import type { Runtime } from './Runtime.type.js'; import type { TimerState } from './TimerState.type.js'; @@ -15,6 +16,7 @@ export type RuntimeStore = { // rundown data runtime: Runtime; + currentBlock: CurrentBlockState; eventNow: OntimeEvent | null; publicEventNow: OntimeEvent | null; eventNext: OntimeEvent | null; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a280d73d20..7041d36b7c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -62,6 +62,7 @@ export type { Message, TimerMessage, MessageState } from './definitions/runtime/ export type { Runtime } from './definitions/runtime/Runtime.type.js'; export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js'; +export type { CurrentBlockState } from './definitions/runtime/CurrentBlockState.type.js'; // ---> Extra Timer export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js'; diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 45768cd067..4922c54345 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -8,6 +8,8 @@ export { sanitiseCue } from './src/cue-utils/cueUtils.js'; export { getCueCandidate } from './src/cue-utils/cueUtils.js'; export { generateId } from './src/generate-id/generateId.js'; export { + filterPlayable, + filterTimedEvents, getFirst, getFirstEvent, getFirstEventNormal, @@ -23,6 +25,7 @@ export { getPreviousEvent, getPreviousEventNormal, getPreviousNormal, + getRelevantBlock, swapEventData, } from './src/rundown-utils/rundownUtils.js'; diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts index e1abc309f5..0e1030c35c 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.test.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts @@ -2,12 +2,15 @@ import type { NormalisedRundown, OntimeEvent, OntimeRundown } from 'ontime-types import { SupportedEvent } from 'ontime-types'; import { + filterPlayable, + filterTimedEvents, getLastEvent, getLastNormal, getNext, getNextEvent, getPrevious, getPreviousEvent, + getRelevantBlock, swapEventData, } from './rundownUtils'; @@ -262,4 +265,59 @@ describe('getLastEvent', () => { 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 }, + ]; + + it('returns the relevant block', () => { + const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'h'); + + expect(block?.id).toBe('g'); + }); + it('returns the relevant block', () => { + const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'f'); + + expect(block?.id).toBe('e'); + }); + it('returns the relevant block', () => { + const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'a'); + + expect(block).toBeNull(); + }); + it('also works on index 0', () => { + testRundown.unshift({ id: '0', type: SupportedEvent.Block }); + const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'a'); + expect(block?.id).toBe('0'); + }); + }); + + describe('filter event', () => { + 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 }, + ]; + + test('filterPlayable', () => { + const result = filterPlayable(testRundown as unknown as OntimeRundown); + expect(result).toMatchObject([eventA]); + }); + + test('filterTimedEvents', () => { + const result = filterTimedEvents(testRundown as unknown as OntimeRundown); + expect(result).toMatchObject([eventA, eventB]); + }); + }); }); diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts index d002bcd61c..a9b85a3999 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -1,5 +1,5 @@ -import type { NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; -import { isOntimeEvent } from 'ontime-types'; +import type { NormalisedRundown, OntimeBlock, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; +import { isOntimeBlock, isOntimeEvent } from 'ontime-types'; type IndexAndEntry = { entry: OntimeRundownEntry | null; index: number | null }; @@ -326,3 +326,44 @@ export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): { newA: return { newA, newB }; }; + +/** + * Gets relevant block element for a given ID + * @param rundown + * @param order + * @param {string} currentId + * @return {OntimeBlock | null} + */ +export function getRelevantBlock(rundown: OntimeRundown, currentId: string): OntimeBlock | null { + let inBlock = false; + // Iterate backwards through the rundown to find the current event + for (let i = rundown.length - 1; i >= 0; i--) { + const entry = rundown[i]; + if (entry.id === currentId) { + //set the flag when the current event is found + inBlock = true; + } + //the first block before the current event is the relevant one + if (inBlock && isOntimeBlock(entry)) { + return entry; + } + } + //no blocks exist before current event + return null; +} + +/** + * returns all events that can be loaded + * @return {array} + */ +export function filterPlayable(rundown: OntimeRundown): OntimeEvent[] { + return rundown.filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[]; +} + +/** + * returns all events of type OntimeEvent + * @return {array} + */ +export function filterTimedEvents(rundown: OntimeRundown): OntimeEvent[] { + return rundown.filter((event) => isOntimeEvent(event)) as OntimeEvent[]; +}