From b88c2a6bd80f921ebfb4840bf7c783f14f6d9b56 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 15 Dec 2024 21:25:41 +0100 Subject: [PATCH] refactor: extract time-to-end --- apps/client/src/common/hooks/useSocket.ts | 1 + .../src/common/models/TimeManager.type.ts | 1 + apps/client/src/common/utils/eventsManager.ts | 1 + .../interface-panel/EditorSettingsForm.tsx | 1 - .../features/control/message/TimerPreview.tsx | 16 +- .../editor-utils/EditorUtils.module.scss | 2 +- .../src/features/rundown/RundownEntry.tsx | 3 +- .../rundown/event-block/EventBlock.tsx | 5 +- .../rundown/event-block/EventBlockInner.tsx | 21 +- .../event-editor/EventEditor.module.scss | 4 +- .../rundown/event-editor/EventEditor.tsx | 1 + .../composite/EventEditorTimes.tsx | 197 ++++++++++-------- .../composite/EventEditorTitles.tsx | 1 + .../rundown/time-input-flow/TimeInputFlow.tsx | 14 +- .../src/features/viewers/ViewWrapper.tsx | 17 +- .../src/features/viewers/common/viewUtils.ts | 10 +- .../viewers/minimal-timer/MinimalTimer.tsx | 2 +- .../src/features/viewers/timer/Timer.tsx | 2 +- apps/server/src/models/demoProject.ts | 15 +- apps/server/src/models/eventsDefinition.ts | 1 + .../src/services/__tests__/timerUtils.test.ts | 23 +- .../__tests__/rundownCache.test.ts | 12 ++ .../__tests__/sheetUtils.test.ts | 6 + apps/server/src/services/timerUtils.ts | 14 +- .../server/src/utils/__tests__/parser.test.ts | 131 +++++++----- .../utils/__tests__/parserFunctions.test.ts | 104 +++++---- apps/server/src/utils/parser.ts | 19 +- apps/server/src/utils/parserFunctions.ts | 37 +++- e2e/tests/fixtures/test-sheet.xlsx | Bin 6621 -> 12735 bytes .../types/src/definitions/TimerType.type.ts | 1 - .../src/definitions/core/OntimeEvent.type.ts | 1 + .../src/validate-events/validateEvent.test.ts | 4 +- 32 files changed, 419 insertions(+), 248 deletions(-) diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index ee56a8b5a..815b5a392 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -65,6 +65,7 @@ export const useMessagePreview = () => { showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external), showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text), timerType: state.eventNow?.timerType ?? null, + isTimeToEnd: state.eventNow?.isTimeToEnd ?? false, }); return useRuntimeStore(featureSelector); diff --git a/apps/client/src/common/models/TimeManager.type.ts b/apps/client/src/common/models/TimeManager.type.ts index 909dfb68c..46bd672a5 100644 --- a/apps/client/src/common/models/TimeManager.type.ts +++ b/apps/client/src/common/models/TimeManager.type.ts @@ -15,4 +15,5 @@ export type ViewExtendedTimer = { clock: number; timerType: TimerType; + isTimeToEnd: boolean; }; diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index 485648919..eb0370b26 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -17,6 +17,7 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { timeEnd: event.timeEnd, timerType: event.timerType, timeStrategy: event.timeStrategy, + isTimeToEnd: event.isTimeToEnd, linkStart: event.linkStart, endAction: event.endAction, isPublic: event.isPublic, diff --git a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx index e865ca67e..f6bcd00f6 100644 --- a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx @@ -88,7 +88,6 @@ export default function EditorSettingsForm() { > - diff --git a/apps/client/src/features/control/message/TimerPreview.tsx b/apps/client/src/features/control/message/TimerPreview.tsx index 4c3765ad5..9eaac95d5 100644 --- a/apps/client/src/features/control/message/TimerPreview.tsx +++ b/apps/client/src/features/control/message/TimerPreview.tsx @@ -16,7 +16,7 @@ import { Corner } from '../../editors/editor-utils/EditorUtils'; import style from './MessageControl.module.scss'; export default function TimerPreview() { - const { blink, blackout, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } = + const { blink, blackout, isTimeToEnd, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } = useMessagePreview(); const { data } = useViewSettings(); @@ -24,11 +24,11 @@ export default function TimerPreview() { const main = (() => { if (showTimerMessage) return 'Message'; + if (timerType === TimerType.None) return timerPlaceholder; if (phase === TimerPhase.Pending) return 'Standby to start'; if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message'; - if (timerType === TimerType.TimeToEnd) return 'Time to end'; if (timerType === TimerType.Clock) return 'Clock'; - if (timerType === TimerType.None) return timerPlaceholder; + if (isTimeToEnd) return 'Target event scheduled end'; return 'Timer'; })(); @@ -74,12 +74,16 @@ export default function TimerPreview() { - - - + + + ); diff --git a/apps/client/src/features/editors/editor-utils/EditorUtils.module.scss b/apps/client/src/features/editors/editor-utils/EditorUtils.module.scss index b2a4f075a..cb6f1c45a 100644 --- a/apps/client/src/features/editors/editor-utils/EditorUtils.module.scss +++ b/apps/client/src/features/editors/editor-utils/EditorUtils.module.scss @@ -27,7 +27,7 @@ .title { font-size: 1rem; - color: $label-gray; + color: $ui-white; } .label { diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 5df99938c..1166961f1 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -150,6 +150,7 @@ export default function RundownEntry(props: RundownEntryProps) { if (data.type === SupportedEvent.Event) { return ( { +function EventBlockInner(props: EventBlockInnerProps) { const { + eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, - eventId, + isTimeToEnd, isPublic = true, endAction, timerType, @@ -91,7 +93,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { delay={delay} timeStrategy={timeStrategy} linkStart={linkStart} - timerType={timerType} + isTimeToEnd={isTimeToEnd} />
@@ -122,6 +124,11 @@ const EventBlockInner = (props: EventBlockInnerProps) => { + + + + + @@ -131,7 +138,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
); -}; +} export default memo(EventBlockInner); @@ -162,9 +169,5 @@ function TimerIcon(props: { type: TimerType; className: string }) { if (type === TimerType.None) { return ; } - if (type === TimerType.TimeToEnd) { - const classes = cx([style.active, className]); - return ; - } return ; } diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss index 99d5b4edc..7f9b8469b 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss @@ -16,7 +16,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 2rem; + gap: 1rem; overflow-y: auto; } @@ -38,6 +38,7 @@ display: flex; flex-direction: column; gap: 1rem; + margin-top: 0.5rem; } .decorated { @@ -63,6 +64,7 @@ gap: 0.5rem; max-width: max-content; cursor: pointer; + height: 30px; // manually match the height of a text input } .inline { diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.tsx b/apps/client/src/features/rundown/event-editor/EventEditor.tsx index 7cb180669..15f8efa3f 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/event-editor/EventEditor.tsx @@ -83,6 +83,7 @@ export default function EventEditor() { duration={event.duration} timeStrategy={event.timeStrategy} linkStart={event.linkStart} + isTimeToEnd={event.isTimeToEnd} delay={event.delay ?? 0} isPublic={event.isPublic} endAction={event.endAction} diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx index 671db1883..6a5e6e8d5 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx @@ -18,6 +18,7 @@ interface EventEditorTimesProps { duration: number; timeStrategy: TimeStrategy; linkStart: MaybeString; + isTimeToEnd: boolean; delay: number; isPublic: boolean; endAction: EndAction; @@ -26,9 +27,9 @@ interface EventEditorTimesProps { timeDanger: number; } -type HandledActions = 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger'; +type HandledActions = 'isTimeToEnd' | 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger'; -const EventEditorTimes = (props: EventEditorTimesProps) => { +function EventEditorTimes(props: EventEditorTimesProps) { const { eventId, timeStart, @@ -36,6 +37,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { duration, timeStrategy, linkStart, + isTimeToEnd, delay, isPublic, endAction, @@ -51,6 +53,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { return; } + if (field === 'isTimeToEnd') { + updateEvent({ id: eventId, isTimeToEnd: !(value as boolean) }); + return; + } + if (field === 'timeWarning' || field === 'timeDanger') { const newTime = parseUserTime(value as string); updateEvent({ id: eventId, [field]: newTime }); @@ -71,95 +78,115 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { : ''; return ( -
-
- Event schedule -
- + <> +
+
+ Event schedule +
+ +
+
{delayLabel}
-
{delayLabel}
-
-
-
- Warning Time - -
-
- Timer Type - -
-
- Danger Time - -
-
- End Action - + Event behaviour +
+
+ End Action + +
+
+ Target Event Scheduled End + + handleSubmit('isTimeToEnd', isTimeToEnd)} + variant='ontime' + /> + {isTimeToEnd ? 'On' : 'Off'} + +
+
+ Display options +
+
+ Timer Type + +
+
+ Warning Time + +
-
- Event Visibility - - handleSubmit('isPublic', isPublic)} - variant='ontime' - /> - {isPublic ? 'Public' : 'Private'} - +
+ Event Visibility + + handleSubmit('isPublic', isPublic)} + variant='ontime' + /> + {isPublic ? 'Public' : 'Private'} + +
+
+ Danger Time + +
+
-
+ ); -}; +} export default memo(EventEditorTimes); diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx index 02c061238..73dd88764 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx @@ -29,6 +29,7 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => { return (
+ Event data
Event ID (read only) diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx index b68a43978..f71cb1b27 100644 --- a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx @@ -5,7 +5,7 @@ import { IoLink } from '@react-icons/all-files/io5/IoLink'; import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed'; import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline'; import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink'; -import { MaybeString, TimerType, TimeStrategy } from 'ontime-types'; +import { MaybeString, TimeStrategy } from 'ontime-types'; import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton'; import { useEventAction } from '../../../common/hooks/useEventAction'; @@ -16,19 +16,19 @@ import style from './TimeInputFlow.module.scss'; interface EventBlockTimerProps { eventId: string; + isTimeToEnd: boolean; timeStart: number; timeEnd: number; duration: number; timeStrategy: TimeStrategy; linkStart: MaybeString; delay: number; - timerType: TimerType; } type TimeActions = 'timeStart' | 'timeEnd' | 'duration'; -const TimeInputFlow = (props: EventBlockTimerProps) => { - const { eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, timerType } = props; +function TimeInputFlow(props: EventBlockTimerProps) { + const { eventId, isTimeToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay } = props; const { updateEvent, updateTimer } = useEventAction(); // In sync with EventEditorTimes @@ -49,8 +49,8 @@ const TimeInputFlow = (props: EventBlockTimerProps) => { warnings.push('Over midnight'); } - if (timerType === TimerType.TimeToEnd) { - warnings.push('Time to end'); + if (isTimeToEnd) { + warnings.push('Target event scheduled end'); } const hasDelay = delay !== 0; @@ -128,6 +128,6 @@ const TimeInputFlow = (props: EventBlockTimerProps) => { )} ); -}; +} export default memo(TimeInputFlow); diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index a0b78bd9b..eb6819e20 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -9,6 +9,7 @@ import { Settings, SimpleTimerState, SupportedEvent, + TimerType, ViewSettings, } from 'ontime-types'; import { useStore } from 'zustand'; @@ -74,16 +75,14 @@ const withData =

(Component: ComponentType

) => { const selectedId = eventNow?.id ?? null; const nextId = eventNext?.id ?? null; - /******************************************/ - /*** + TimeManagerType ***/ - /*** WRAP INFORMATION RELATED TO TIME ***/ - /*** -------------------------------- ***/ - /******************************************/ - - const TimeManagerType = { + /** + * Contains an extended timer object with properties from the current event + */ + const timeManagerType: ViewExtendedTimer = { ...timer, clock, - timerType: eventNow?.timerType ?? null, + timerType: eventNow?.timerType ?? TimerType.CountDown, + isTimeToEnd: eventNow?.isTimeToEnd ?? false, }; return ( @@ -108,7 +107,7 @@ const withData =

(Component: ComponentType

) => { runtime={runtime} selectedId={selectedId} settings={settings} - time={TimeManagerType} + time={timeManagerType} viewSettings={viewSettings} /> diff --git a/apps/client/src/features/viewers/common/viewUtils.ts b/apps/client/src/features/viewers/common/viewUtils.ts index 32158f01c..3111d2cdb 100644 --- a/apps/client/src/features/viewers/common/viewUtils.ts +++ b/apps/client/src/features/viewers/common/viewUtils.ts @@ -5,16 +5,22 @@ import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type' import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils'; import { formatTime } from '../../../common/utils/time'; -type TimerTypeParams = Pick; +type TimerTypeParams = Pick; export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null { if (!timerObject) { return null; } + if (timerObject.isTimeToEnd) { + if (timerObject.current === null) { + return null; + } + return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current; + } + switch (timerObject.timerType) { case TimerType.CountDown: - case TimerType.TimeToEnd: if (timerObject.current === null) { return null; } diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 633b0aee7..83cc0f6d7 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -124,7 +124,7 @@ export default function MinimalTimer(props: MinimalTimerProps) { const isPlaying = time.playback !== Playback.Pause; - const shouldShowModifiers = time.timerType === TimerType.CountDown || time.timerType === TimerType.TimeToEnd; + const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd; const finished = time.phase === TimerPhase.Overtime; const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage; const showFinished = diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index d0b82cd22..a32052cb3 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -118,7 +118,7 @@ export default function Timer(props: TimerProps) { const finished = time.phase === TimerPhase.Overtime; const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0); - const shouldShowModifiers = time.timerType === TimerType.CountDown || time.timerType === TimerType.TimeToEnd; + const shouldShowModifiers = time.timerType === TimerType.CountDown || time.isTimeToEnd; const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage; const showProgress = eventNow !== null && diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index 78ed0cef9..91e3abdcc 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -10,6 +10,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.01', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 36000000, @@ -34,6 +35,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.02', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 37500000, @@ -58,6 +60,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.03', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 39000000, @@ -82,6 +85,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.04', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 40500000, @@ -106,6 +110,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.05', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 42000000, @@ -135,6 +140,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.06', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 47100000, @@ -159,6 +165,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.07', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 48600000, @@ -183,6 +190,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.08', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 50100000, @@ -207,6 +215,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.09', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 51600000, @@ -231,6 +240,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.10', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 53100000, @@ -260,6 +270,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.11', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 56100000, @@ -284,9 +295,9 @@ export const demoDb: DatabaseModel = { note: 'SF1.12', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, - timeStart: 57600000, timeEnd: 58800000, duration: 1200000, @@ -309,6 +320,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.13', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 59100000, @@ -333,6 +345,7 @@ export const demoDb: DatabaseModel = { note: 'SF1.14', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, linkStart: null, timeStrategy: TimeStrategy.LockEnd, timeStart: 60600000, diff --git a/apps/server/src/models/eventsDefinition.ts b/apps/server/src/models/eventsDefinition.ts index 4760d7c3c..cc8bc563c 100644 --- a/apps/server/src/models/eventsDefinition.ts +++ b/apps/server/src/models/eventsDefinition.ts @@ -15,6 +15,7 @@ export const event: Omit = { timerType: TimerType.CountDown, timeStrategy: TimeStrategy.LockDuration, linkStart: null, + isTimeToEnd: false, timeStart: 0, timeEnd: 0, duration: 0, diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 2f9f418ae..01fd0276c 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -175,7 +175,7 @@ describe('getExpectedFinish()', () => { const state = { eventNow: { timeEnd: 30, - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, timer: { addedTime: 10, @@ -195,7 +195,7 @@ describe('getExpectedFinish()', () => { const state = { eventNow: { timeEnd: 600000, // 00:10:00 - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, timer: { addedTime: 0, @@ -351,7 +351,7 @@ describe('getCurrent()', () => { const state = { eventNow: { timeEnd: 100, - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, clock: 30, timer: { @@ -376,7 +376,7 @@ describe('getCurrent()', () => { const state = { eventNow: { timeEnd: 100, - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, clock: 30, timer: { @@ -401,7 +401,7 @@ describe('getCurrent()', () => { const state = { eventNow: { timeEnd: 100, - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, clock: 30, timer: { @@ -427,7 +427,7 @@ describe('getCurrent()', () => { eventNow: { timeStart: 79200000, // 22:00:00 timeEnd: 600000, // 00:10:00 - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, clock: 79500000, // 22:05:00 timer: { @@ -456,7 +456,7 @@ describe('getCurrent()', () => { timeStart: 77400000, // 21:30:00 timeEnd: 81000000, // 22:30:00 duration: 3600000, // 01:00:00 - timerType: TimerType.TimeToEnd, + isTimeToEnd: true, }, timer: { addedTime: 0, @@ -909,7 +909,8 @@ describe('getRuntimeOffset()', () => { timeStrategy: TimeStrategy.LockEnd, linkStart: null, endAction: EndAction.None, - timerType: TimerType.TimeToEnd, + timerType: TimerType.CountDown, + isTimeToEnd: true, isPublic: true, skip: false, note: '', @@ -961,7 +962,8 @@ describe('getRuntimeOffset()', () => { timeStrategy: TimeStrategy.LockEnd, linkStart: null, endAction: EndAction.None, - timerType: TimerType.TimeToEnd, + timerType: TimerType.CountDown, + isTimeToEnd: true, isPublic: true, skip: false, note: '', @@ -1011,7 +1013,8 @@ describe('getRuntimeOffset()', () => { timeStrategy: TimeStrategy.LockEnd, linkStart: null, endAction: EndAction.None, - timerType: TimerType.TimeToEnd, // <--- but this is time to end + timerType: TimerType.CountDown, + isTimeToEnd: true, }, runtime: { selectedEventIndex: 0, diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index c6af0745b..5eccaeacc 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -565,6 +565,7 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -591,6 +592,7 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, @@ -617,6 +619,7 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -643,6 +646,7 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, @@ -678,6 +682,7 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -705,6 +710,7 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, @@ -732,6 +738,7 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -759,6 +766,7 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, @@ -812,6 +820,7 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -839,6 +848,7 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, @@ -866,6 +876,7 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 600000, @@ -893,6 +904,7 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, timeStart: 1200000, diff --git a/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts b/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts index 3304c74c1..c4fb5c6b0 100644 --- a/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts +++ b/apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts @@ -31,6 +31,7 @@ describe('cellRequestFromEvent()', () => { linkStart: null, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, duration: 10800000, isPublic: false, skip: false, @@ -73,6 +74,7 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, duration: 10800000, timeStrategy: TimeStrategy.LockEnd, linkStart: null, @@ -119,6 +121,7 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, duration: 10800000, timeStrategy: TimeStrategy.LockEnd, linkStart: null, @@ -164,6 +167,7 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, timeStrategy: TimeStrategy.LockEnd, linkStart: null, duration: 10800000, @@ -195,6 +199,7 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, duration: 10800000, timeStrategy: TimeStrategy.LockEnd, linkStart: null, @@ -227,6 +232,7 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + isTimeToEnd: false, duration: 10800000, timeStrategy: TimeStrategy.LockEnd, linkStart: null, diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 28fd41263..df7d3f7c2 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -1,4 +1,4 @@ -import { MaybeNumber, Playback, TimerPhase, TimerType } from 'ontime-types'; +import { MaybeNumber, Playback, TimerPhase } from 'ontime-types'; import { dayInMs } from 'ontime-utils'; import { RuntimeState } from '../stores/runtimeState.js'; @@ -19,7 +19,7 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber { return null; } - const { timerType, timeEnd } = state.eventNow; + const { isTimeToEnd, timeEnd } = state.eventNow; const { pausedAt } = state._timer; const { clock } = state; @@ -33,7 +33,7 @@ export function getExpectedFinish(state: RuntimeState): MaybeNumber { const pausedTime = pausedAt != null ? clock - pausedAt : 0; - if (timerType === TimerType.TimeToEnd) { + if (isTimeToEnd) { return timeEnd + addedTime + pausedTime; } @@ -62,11 +62,11 @@ export function getCurrent(state: RuntimeState): number { } } const { startedAt, duration, addedTime } = state.timer; - const { timerType, timeStart, timeEnd } = state.eventNow; + const { isTimeToEnd, timeStart, timeEnd } = state.eventNow; const { pausedAt } = state._timer; const { clock } = state; - if (timerType === TimerType.TimeToEnd) { + if (isTimeToEnd) { const isEventOverMidnight = timeStart > timeEnd; const correctDay = isEventOverMidnight ? dayInMs : 0; return correctDay - clock + timeEnd + addedTime; @@ -131,7 +131,7 @@ export function getRuntimeOffset(state: RuntimeState): number { } const { clock } = state; - const { timeStart, timerType } = state.eventNow; + const { isTimeToEnd, timeStart } = state.eventNow; const { addedTime, current, startedAt } = state.timer; // if we havent started, but the timer is armed @@ -142,7 +142,7 @@ export function getRuntimeOffset(state: RuntimeState): number { const overtime = Math.min(current, 0); // in time-to-end, offset is overtime - if (timerType === TimerType.TimeToEnd) { + if (isTimeToEnd) { return overtime; } diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts index dc9d765e0..f8b32b1f3 100644 --- a/apps/server/src/utils/__tests__/parser.test.ts +++ b/apps/server/src/utils/__tests__/parser.test.ts @@ -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, + }); }); }); diff --git a/apps/server/src/utils/__tests__/parserFunctions.test.ts b/apps/server/src/utils/__tests__/parserFunctions.test.ts index ad0de0937..7a6e03824 100644 --- a/apps/server/src/utils/__tests__/parserFunctions.test.ts +++ b/apps/server/src/utils/__tests__/parserFunctions.test.ts @@ -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 = { 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, + }); }); }); diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index db410bfb6..e9822f984 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -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, cueFallback: string): OntimeEvent | null => { +export const createEvent = (eventArgs: Partial, 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); diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index dadde53bb..1889ac250 100644 --- a/apps/server/src/utils/parserFunctions.ts +++ b/apps/server/src/utils/parserFunctions.ts @@ -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); +} diff --git a/e2e/tests/fixtures/test-sheet.xlsx b/e2e/tests/fixtures/test-sheet.xlsx index aa335bea13947f2caca605f2bb0e5578e28908f5..a5f2a8f96911b0c05bd5ac528d8c263c9d640d1c 100644 GIT binary patch literal 12735 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 G_Wu9{1X(Kp diff --git a/packages/types/src/definitions/TimerType.type.ts b/packages/types/src/definitions/TimerType.type.ts index e9bae59fb..df9e0757d 100644 --- a/packages/types/src/definitions/TimerType.type.ts +++ b/packages/types/src/definitions/TimerType.type.ts @@ -1,7 +1,6 @@ export enum TimerType { CountDown = 'count-down', CountUp = 'count-up', - TimeToEnd = 'time-to-end', Clock = 'clock', None = 'none', } diff --git a/packages/types/src/definitions/core/OntimeEvent.type.ts b/packages/types/src/definitions/core/OntimeEvent.type.ts index ac9d08a25..6cef9896c 100644 --- a/packages/types/src/definitions/core/OntimeEvent.type.ts +++ b/packages/types/src/definitions/core/OntimeEvent.type.ts @@ -29,6 +29,7 @@ export type OntimeEvent = OntimeBaseEvent & { note: string; endAction: EndAction; timerType: TimerType; + isTimeToEnd: boolean; linkStart: MaybeString; // ID of event to link to timeStrategy: TimeStrategy; timeStart: number; diff --git a/packages/utils/src/validate-events/validateEvent.test.ts b/packages/utils/src/validate-events/validateEvent.test.ts index c7fa207fb..7ccd77153 100644 --- a/packages/utils/src/validate-events/validateEvent.test.ts +++ b/packages/utils/src/validate-events/validateEvent.test.ts @@ -18,8 +18,8 @@ describe('validateEndAction()', () => { describe('validateTimerType()', () => { it('recognises a string representation of an action', () => { - const timerType = validateTimerType('time-to-end'); - expect(timerType).toBe(TimerType.TimeToEnd); + const timerType = validateTimerType('count-up'); + expect(timerType).toBe(TimerType.CountUp); }); it('returns fallback otherwise', () => { const emptyType = validateTimerType('', TimerType.Clock);