diff --git a/apps/client/src/features/menu/MenuBar.tsx b/apps/client/src/features/menu/MenuBar.tsx
index 7accb71aa..a52240157 100644
--- a/apps/client/src/features/menu/MenuBar.tsx
+++ b/apps/client/src/features/menu/MenuBar.tsx
@@ -9,9 +9,6 @@ import { cx } from '../../common/utils/styleUtils';
import style from './MenuBar.module.scss';
interface MenuBarProps {
- isOldSettingsOpen: boolean;
- onSettingsOpen: () => void;
- onSettingsClose: () => void;
openSettings: (newTab?: string) => void;
isSettingsOpen: boolean;
}
@@ -29,7 +26,7 @@ const buttonStyle = {
};
const MenuBar = (props: MenuBarProps) => {
- const { onSettingsOpen, onSettingsClose, openSettings, isSettingsOpen } = props;
+ const { openSettings, isSettingsOpen } = props;
const { isElectron, sendToElectron } = useElectronEvent();
const sendShutdown = () => {
@@ -48,14 +45,13 @@ const MenuBar = (props: MenuBarProps) => {
if (event.ctrlKey || event.metaKey) {
// ctrl + , (settings)
if (event.key === ',') {
- // open if not open
- isSettingsOpen ? onSettingsClose() : onSettingsOpen();
+ openSettings();
event.preventDefault();
event.stopPropagation();
}
}
},
- [isSettingsOpen, onSettingsClose, onSettingsOpen],
+ [openSettings],
);
useEffect(() => {
diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx
index 7a30d056a..ab239c77d 100644
--- a/apps/client/src/features/rundown/Rundown.tsx
+++ b/apps/client/src/features/rundown/Rundown.tsx
@@ -209,9 +209,11 @@ export default function Rundown({ data }: RundownProps) {
return
insertAtCursor(SupportedEvent.Event, null)} />;
}
+ let previousStart: MaybeNumber = null;
let previousEnd: MaybeNumber = null;
let previousEventId: string | undefined;
- let thisEnd: MaybeNumber = previousEnd;
+ let thisStart: MaybeNumber = null;
+ let thisEnd: MaybeNumber = null;
let thisId = previousEventId;
let eventIndex = 0;
@@ -237,10 +239,12 @@ export default function Rundown({ data }: RundownProps) {
if (isOntimeEvent(event)) {
// event indexes are 1 based in frontend
eventIndex++;
+ previousStart = thisStart;
previousEnd = thisEnd;
previousEventId = thisId;
if (!event.skip) {
+ thisStart = event.timeStart;
thisEnd = event.timeEnd;
thisId = eventId;
}
@@ -266,6 +270,7 @@ export default function Rundown({ data }: RundownProps) {
loaded={isLoaded}
hasCursor={hasCursor}
isNext={isNext}
+ previousStart={previousStart}
previousEnd={previousEnd}
previousEventId={previousEventId}
playback={isLoaded ? featureData.playback : undefined}
diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx
index e20f06875..9babf3d49 100644
--- a/apps/client/src/features/rundown/RundownEntry.tsx
+++ b/apps/client/src/features/rundown/RundownEntry.tsx
@@ -23,6 +23,7 @@ interface RundownEntryProps {
eventIndex: number;
hasCursor: boolean;
isNext: boolean;
+ previousStart: MaybeNumber;
previousEnd: MaybeNumber;
previousEventId?: string;
playback?: Playback; // we only care about this if this event is playing
@@ -30,8 +31,19 @@ interface RundownEntryProps {
}
export default function RundownEntry(props: RundownEntryProps) {
- const { isPast, data, loaded, hasCursor, isNext, previousEnd, previousEventId, playback, isRolling, eventIndex } =
- props;
+ const {
+ isPast,
+ data,
+ loaded,
+ hasCursor,
+ isNext,
+ previousStart,
+ previousEnd,
+ previousEventId,
+ playback,
+ isRolling,
+ eventIndex,
+ } = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
const { cursor } = useAppMode();
@@ -135,6 +147,7 @@ export default function RundownEntry(props: RundownEntryProps) {
title={data.title}
note={data.note}
delay={data.delay ?? 0}
+ previousStart={previousStart}
previousEnd={previousEnd}
colour={data.colour}
isPast={isPast}
diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx
index 7f30f6578..67a84ab3e 100644
--- a/apps/client/src/features/rundown/event-block/EventBlock.tsx
+++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx
@@ -37,6 +37,7 @@ interface EventBlockProps {
title: string;
note: string;
delay: number;
+ previousStart: MaybeNumber;
previousEnd: MaybeNumber;
colour: string;
isPast: boolean;
@@ -73,6 +74,7 @@ export default function EventBlock(props: EventBlockProps) {
title,
note,
delay,
+ previousStart,
previousEnd,
colour,
isPast,
@@ -244,7 +246,13 @@ export default function EventBlock(props: EventBlockProps) {
onContextMenu={onContextMenu}
id='event-block'
>
-
+
diff --git a/apps/client/src/features/rundown/event-block/EventBlock.utils.ts b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts
new file mode 100644
index 000000000..bfb8a67ae
--- /dev/null
+++ b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts
@@ -0,0 +1,34 @@
+import { MaybeNumber } from 'ontime-types';
+import { millisToString, removeLeadingZero, removeTrailingZero } from 'ontime-utils';
+
+export function formatDelay(timeStart: number, delay: number): string | undefined {
+ if (!delay) return;
+
+ const delayedStart = Math.max(0, timeStart + delay);
+
+ const timeTag = removeTrailingZero(millisToString(delayedStart));
+ return `New start ${timeTag}`;
+}
+
+export function formatOverlap(
+ previousStart: MaybeNumber,
+ previousEnd: MaybeNumber,
+ timeStart: number,
+ timeEnd: number,
+): string | undefined {
+ if (previousEnd === null) return;
+
+ const overlap = previousEnd - timeStart;
+ if (overlap === 0) return;
+
+ if (previousStart && timeStart < previousEnd) {
+ const overlap = timeEnd - previousStart;
+ if (overlap <= 0) return;
+
+ const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
+ return `Overlap ${overlapString}`;
+ }
+
+ const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
+ return `${overlap > 0 ? 'Overlap' : 'Gap'} ${overlapString}`;
+}
diff --git a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx
index b557231ba..5f3120cbb 100644
--- a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx
+++ b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx
@@ -1,36 +1,21 @@
-import { millisToString, removeLeadingZero, removeTrailingZero } from 'ontime-utils';
+import { MaybeNumber } from 'ontime-types';
+
+import { formatDelay, formatOverlap } from './EventBlock.utils';
import style from './RundownIndicators.module.scss';
interface RundownIndicatorProps {
timeStart: number;
- previousEnd: number | null;
+ timeEnd: number;
+ previousStart: MaybeNumber;
+ previousEnd: MaybeNumber;
delay: number;
}
-function formatDelay(timeStart: number, delay: number): string | undefined {
- if (!delay) return;
-
- const delayedStart = Math.max(0, timeStart + delay);
- const timeTag = removeTrailingZero(millisToString(delayedStart));
- return `New start ${timeTag}`;
-}
-
-function formatOverlap(previousEnd: number | null, timeStart: number): string | undefined {
- if (previousEnd === null) return;
-
- const overlap = previousEnd - timeStart;
- if (overlap === 0) return;
-
- const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
-
- return `${overlap > 0 ? 'Overlap' : 'Gap'} ${overlapString}`;
-}
-
export default function RundownIndicators(props: RundownIndicatorProps) {
- const { timeStart, previousEnd, delay } = props;
+ const { timeStart, timeEnd, previousStart, previousEnd, delay } = props;
- const hasOverlap = formatOverlap(previousEnd, timeStart);
+ const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
const hasDelay = formatDelay(timeStart, delay);
return (
diff --git a/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts b/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts
new file mode 100644
index 000000000..1b77e7512
--- /dev/null
+++ b/apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts
@@ -0,0 +1,39 @@
+import { formatDelay, formatOverlap } from '../EventBlock.utils';
+
+describe('formatDelay()', () => {
+ it('adds a given delay to the start time', () => {
+ const timeStart = 60000; // 1 min
+ const delay = 60000; // 1 min
+ const result = formatDelay(timeStart, delay);
+ expect(result).toEqual('New start 00:02');
+ });
+});
+
+describe('formatOverlap()', () => {
+ it('recognises an overlap between two times', () => {
+ const previousStart = 0;
+ const previousEnd = 60000; // 1 min
+ const timeStart = 30000; // 30 sec
+ const timeEnd = 90000; // 1:30 min
+ const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
+ expect(result).toEqual('Overlap 0:30');
+ });
+
+ it('handles events the day after, without overlap', () => {
+ const previousStart = new Date(0).setUTCHours(11).valueOf();
+ const previousEnd = new Date(0).setUTCHours(12).valueOf();
+ const timeStart = new Date(0).setUTCHours(6).valueOf();
+ const timeEnd = new Date(0).setUTCHours(10).valueOf();
+ const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
+ expect(result).toBeUndefined();
+ });
+
+ it('handles events the day after, with overlap', () => {
+ const previousStart = new Date(0).setUTCHours(9).valueOf();
+ const previousEnd = new Date(0).setUTCHours(10).valueOf();
+ const timeStart = new Date(0).setUTCHours(6).valueOf();
+ const timeEnd = new Date(0).setUTCHours(11).valueOf();
+ const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
+ expect(result).toBe('Overlap 02:00:00');
+ });
+});
diff --git a/apps/client/src/features/viewers/studio/StudioClock.scss b/apps/client/src/features/viewers/studio/StudioClock.scss
index 56744eef3..c5c4b1084 100644
--- a/apps/client/src/features/viewers/studio/StudioClock.scss
+++ b/apps/client/src/features/viewers/studio/StudioClock.scss
@@ -41,161 +41,161 @@ $orange-active: #f60;
gap: 2.5vw;
grid-template-areas: 'clock schedule';
text-transform: uppercase;
-}
-.clock-container {
- grid-area: clock;
- display: grid;
- place-content: center;
- justify-items: center;
- padding-top: 3em;
- gap: 0.5em;
- width: 100%;
- height: 100%;
- position: relative;
- font-family: seven-seg;
-}
+ .clock-container {
+ grid-area: clock;
+ display: grid;
+ place-content: center;
+ justify-items: center;
+ padding-top: 3em;
+ gap: 0.5em;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ font-family: seven-seg;
+ }
-.clock-indicators {
- position: absolute;
- top: 0;
- width: 100%;
- height: 100%;
-
- .min,
- .hours {
- border-radius: 50%;
+ .clock-indicators {
position: absolute;
- background: var(--studio-idle, $red-idle);
+ top: 0;
+ width: 100%;
+ height: 100%;
- &--active {
- background: var(--studio-active, $red-active);
+ .min,
+ .hours {
+ border-radius: 50%;
+ position: absolute;
+ background: var(--studio-idle, $red-idle);
+
+ &--active {
+ background: var(--studio-active, $red-active);
+ }
+ }
+
+ .min {
+ min-height: $size-min;
+ width: $size-min;
+ top: calc(50% - #{$half_min});
+ left: calc(50% - #{$half_min});
+ }
+
+ .hours {
+ min-height: $size-hours;
+ width: $size-hours;
+ top: calc(50% - #{$half_hours});
+ left: calc(50% - #{$half_hours});
}
}
- .min {
- min-height: $size-min;
- width: $size-min;
- top: calc(50% - #{$half_min});
- left: calc(50% - #{$half_min});
+ .studio-timer {
+ font-size: calc(var(--clock-size) / 5);
+ line-height: 1em;
+
+ color: var(--studio-active, $red-active);
+
+ &--with-seconds {
+ font-size: calc(var(--clock-size) / 6.5);
+ }
+
+ &::before {
+ content: '88:88';
+ mix-blend-mode: hard-light;
+ opacity: 0.1;
+ position: absolute;
+ }
}
- .hours {
- min-height: $size-hours;
- width: $size-hours;
- top: calc(50% - #{$half_hours});
- left: calc(50% - #{$half_hours});
- }
-}
-
-.studio-timer {
- font-size: calc(var(--clock-size) / 5);
- line-height: 1em;
-
- color: var(--studio-active, $red-active);
-
- &--with-seconds {
- font-size: calc(var(--clock-size) / 6.5);
- }
-
- &::before {
- content: '88:88';
- mix-blend-mode: hard-light;
- opacity: 0.1;
- position: absolute;
- }
-}
-
-.clock__ampm {
- font-family: monospace;
- font-size: calc(var(--clock-size) / 20);
- position: relative;
- top: -2em;
- margin-bottom: -2em;
- color: var(--studio-active, $red-active);
-}
-
-.next-title {
- font-family: monospace;
- font-weight: 400;
- color: var(--studio-active-label, $cyan-active);
- font-size: calc(var(--clock-size) / 10);
- text-align: center;
- overflow: clip hidden;
- white-space: nowrap;
-}
-
-.next-countdown {
- color: var(--studio-active, $red-active);
- font-size: calc(var(--clock-size) / 10);
- line-height: 1em;
-
- &--overtime {
+ .clock__ampm {
+ font-family: monospace;
+ font-size: calc(var(--clock-size) / 20);
+ position: relative;
+ top: -2em;
+ margin-bottom: -2em;
color: var(--studio-active, $red-active);
}
- &--paused {
- color: var(--studio-overtime, $orange-active);
- }
-}
-.schedule-container {
- grid-area: schedule;
- font-family: monospace;
- margin-right: 2vw;
-}
-
-.onAir {
- font-size: 13vh;
- padding-top: 6vh;
- padding-bottom: 0.25em;
- line-height: 0.8em;
- color: var(--studio-active, $red-active);
-
- &--idle {
- color: var(--studio-idle, $red-idle);
- }
-}
-
-.schedule {
- color: var(--studio-idle-label, $red-idle);
- font-size: 3.5vh;
- line-height: 1.25em;
- list-style: none;
-}
-
-.schedule__item {
- margin-bottom: 1.5vh;
- padding-left: 0.25em;
- display: flex;
- align-items: start;
- gap: 0.5em;
- white-space: nowrap;
-
- &--now {
- color: var(--studio-active-label, $red-active);
+ .next-title {
+ font-family: monospace;
+ font-weight: 400;
+ color: var(--studio-active-label, $cyan-active);
+ font-size: calc(var(--clock-size) / 10);
+ text-align: center;
+ overflow: clip hidden;
+ white-space: nowrap;
}
- &--next {
- color: var(--studio-active, $cyan-active);
- padding-bottom: 1em;
+ .next-countdown {
+ color: var(--studio-active, $red-active);
+ font-size: calc(var(--clock-size) / 10);
+ line-height: 1em;
+
+ &--overtime {
+ color: var(--studio-active, $red-active);
+ }
+ &--paused {
+ color: var(--studio-overtime, $orange-active);
+ }
}
- &--future {
- color: var(--studio-idle-label, $cyan-idle);
+ .schedule-container {
+ grid-area: schedule;
+ font-family: monospace;
+ margin-right: 2vw;
}
-}
-.event {
- display: flex;
- align-items: center;
- gap: 0.25em;
+ .onAir {
+ font-size: 13vh;
+ padding-top: 6vh;
+ padding-bottom: 0.25em;
+ line-height: 0.8em;
+ color: var(--studio-active, $red-active);
- &__colour {
- width: 0.35em;
- height: 0.35em;
- aspect-ratio: 1;
- border-radius: 0.35em;
- background-color: var(--studio-idle, $red-idle);
+ &--idle {
+ color: var(--studio-idle, $red-idle);
+ }
+ }
+
+ .schedule {
+ color: var(--studio-idle-label, $red-idle);
+ font-size: 3.5vh;
+ line-height: 1.25em;
+ list-style: none;
+ }
+
+ .schedule__item {
+ margin-bottom: 1.5vh;
+ padding-left: 0.25em;
+ display: flex;
+ align-items: start;
+ gap: 0.5em;
+ white-space: nowrap;
+
+ &--now {
+ color: var(--studio-active-label, $red-active);
+ }
+
+ &--next {
+ color: var(--studio-active, $cyan-active);
+ padding-bottom: 1em;
+ }
+
+ &--future {
+ color: var(--studio-idle-label, $cyan-idle);
+ }
+ }
+
+ .event {
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+
+ &__colour {
+ width: 0.35em;
+ height: 0.35em;
+ aspect-ratio: 1;
+ border-radius: 0.35em;
+ background-color: var(--studio-idle, $red-idle);
+ }
}
}
diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts
index b71f213b8..993504bae 100644
--- a/apps/server/src/services/TimerService.ts
+++ b/apps/server/src/services/TimerService.ts
@@ -1,4 +1,4 @@
-import { OntimeEvent, RuntimeStore } from 'ontime-types';
+import { OntimeEvent, Playback, RuntimeStore } from 'ontime-types';
import { deepEqual } from 'fast-equals';
@@ -149,7 +149,10 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback;
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
- eventStore.set('timer', state.timer);
+ eventStore.batchSet({
+ timer: state.timer,
+ onAir: state.timer.playback !== Playback.Stop,
+ });
TimerService.previousState.timer = { ...state.timer };
}
diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts
index 5900d6a1f..089f8236a 100644
--- a/apps/server/src/services/__tests__/timerUtils.test.ts
+++ b/apps/server/src/services/__tests__/timerUtils.test.ts
@@ -1518,7 +1518,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: null,
},
- _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
+ _timer: { pausedAt: null, secondaryTarget: null },
} as RuntimeState;
const offset = getRuntimeOffset(state);
@@ -1570,7 +1570,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: null,
},
- _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
+ _timer: { pausedAt: null, secondaryTarget: null },
} as RuntimeState;
const offset = getRuntimeOffset(state);
@@ -1622,7 +1622,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
- _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
+ _timer: { pausedAt: null, secondaryTarget: null },
} as RuntimeState;
const offset = getRuntimeOffset(state);
@@ -1674,7 +1674,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
- _timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
+ _timer: { pausedAt: null, secondaryTarget: null },
} as RuntimeState;
const offset = getRuntimeOffset(state);
diff --git a/apps/server/src/services/integration-service/HttpIntegration.ts b/apps/server/src/services/integration-service/HttpIntegration.ts
index 67f439d49..906702f6e 100644
--- a/apps/server/src/services/integration-service/HttpIntegration.ts
+++ b/apps/server/src/services/integration-service/HttpIntegration.ts
@@ -34,7 +34,7 @@ export class HttpIntegration implements IIntegration {
dispatch(action: TimerLifeCycleKey, state?: object) {
// noop
- if (!this.enabled || !action) {
+ if (!this.enabled) {
return;
}
diff --git a/apps/server/src/services/integration-service/OscIntegration.ts b/apps/server/src/services/integration-service/OscIntegration.ts
index 62c440450..fd1d737f4 100644
--- a/apps/server/src/services/integration-service/OscIntegration.ts
+++ b/apps/server/src/services/integration-service/OscIntegration.ts
@@ -64,7 +64,7 @@ export class OscIntegration implements IIntegration {
dispatch(action: TimerLifeCycleKey, state?: object) {
// noop
- if (!this.oscClient || !action) {
+ if (!this.oscClient) {
return;
}
diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts
index e91e7b683..a8ee8f97e 100644
--- a/apps/server/src/services/runtime-service/RuntimeService.ts
+++ b/apps/server/src/services/runtime-service/RuntimeService.ts
@@ -29,7 +29,6 @@ class RuntimeService {
/** Checks result of an update and notifies integrations as needed */
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
const newState = runtimeState.getState();
-
if (hasTimerFinished) {
integrationService.dispatch(TimerLifeCycle.onFinish);
@@ -37,11 +36,11 @@ class RuntimeService {
// actions are added to the queue stack to ensure that the order of operations is maintained
if (newState.timer.playback === Playback.Play && newState.eventNow) {
if (newState.eventNow.endAction === EndAction.Stop) {
- setTimeout(this.stop, 0);
+ setTimeout(this.stop.bind(this), 0);
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
- setTimeout(this.loadNext, 0);
+ setTimeout(this.loadNext.bind(this), 0);
} else if (newState.eventNow.endAction === EndAction.PlayNext) {
- setTimeout(this.startNext, 0);
+ setTimeout(this.startNext.bind(this), 0);
}
}
}
diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts
index f3da864b1..f9860fd9d 100644
--- a/apps/server/src/stores/runtimeState.ts
+++ b/apps/server/src/stores/runtimeState.ts
@@ -47,7 +47,6 @@ export type RuntimeState = {
// private properties of the timer calculations
_timer: {
pausedAt: MaybeNumber;
- finishedNow: boolean;
secondaryTarget: MaybeNumber;
};
};
@@ -63,9 +62,6 @@ const runtimeState: RuntimeState = {
_timer: {
pausedAt: null,
secondaryTarget: null,
- get finishedNow() {
- return this.current <= 0 && this.finishedAt === null;
- },
},
};
@@ -92,7 +88,6 @@ export function clear() {
runtimeState._timer = {
pausedAt: null,
secondaryTarget: null,
- finishedNow: false,
};
}
@@ -411,8 +406,9 @@ export function update(): UpdateResult {
function onPlayUpdate() {
let isFinished = false;
runtimeState.timer.current = getCurrent(runtimeState);
+ const finishedNow = runtimeState.timer.current <= 0 && runtimeState.timer.finishedAt === null;
- if (runtimeState.timer.playback === Playback.Play && runtimeState._timer.finishedNow) {
+ if (runtimeState.timer.playback === Playback.Play && finishedNow) {
runtimeState.timer.finishedAt = runtimeState.clock;
isFinished = true;
} else {
diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts
index f75f0e027..2fa338036 100644
--- a/apps/server/src/utils/__tests__/parser.test.ts
+++ b/apps/server/src/utils/__tests__/parser.test.ts
@@ -1106,5 +1106,261 @@ describe('parseExcel()', () => {
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
});
- it.todo('imports events and blocks, ignores otherwise', () => {});
+ it('ignores unknown event types', () => {
+ const testdata = [
+ [
+ 'Time Start',
+ 'Time End',
+ 'Title',
+ 'End Action',
+ 'Timer type',
+ 'Public',
+ 'Skip',
+ 'Notes',
+ 'test0',
+ 'test1',
+ 'test2',
+ 'test3',
+ 'test4',
+ 'test5',
+ 'test6',
+ 'test7',
+ 'test8',
+ 'test9',
+ 'Colour',
+ 'cue',
+ ],
+ [
+ '1899-12-30T07:00:00.000Z',
+ '1899-12-30T08:00:10.000Z',
+ 'Guest Welcome',
+ '',
+ 'skip',
+ 'x',
+ '',
+ 'Ballyhoo',
+ 'a0',
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ 'a7',
+ 'a8',
+ 'a9',
+ 'red',
+ 101,
+ ],
+ [
+ '1899-12-30T08:00:00.000Z',
+ '1899-12-30T08:30:00.000Z',
+ 'A song from the hearth',
+ 'load-next',
+ 'clock',
+ '',
+ 'x',
+ 'Rainbow chase',
+ 'b0',
+ '',
+ '',
+ '',
+ '',
+ 'b5',
+ '',
+ '',
+ '',
+ '',
+ '#F00',
+ 102,
+ ],
+ [],
+ ];
+
+ const importMap = {
+ worksheet: 'event schedule',
+ timeStart: 'time start',
+ timeEnd: 'time end',
+ duration: 'duration',
+ cue: 'cue',
+ title: 'title',
+ isPublic: 'public',
+ skip: 'skip',
+ note: 'notes',
+ colour: 'colour',
+ endAction: 'end action',
+ timerType: 'timer type',
+ timeWarning: 'warning time',
+ timeDanger: 'danger time',
+ custom: {},
+ };
+ const result = parseExcel(testdata, importMap);
+ expect(result.rundown.length).toBe(1);
+ expect((result.rundown.at(0) as OntimeEvent).title).toBe('A song from the hearth');
+ });
+ it('imports blocks', () => {
+ const testdata = [
+ [
+ 'Time Start',
+ 'Time End',
+ 'Title',
+ 'End Action',
+ 'Timer type',
+ 'Public',
+ 'Skip',
+ 'Notes',
+ 'test0',
+ 'test1',
+ 'test2',
+ 'test3',
+ 'test4',
+ 'test5',
+ 'test6',
+ 'test7',
+ 'test8',
+ 'test9',
+ 'Colour',
+ 'cue',
+ ],
+ [
+ '',
+ '',
+ '',
+ '',
+ 'block',
+ 'x',
+ '',
+ 'Ballyhoo',
+ 'a0',
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ 'a7',
+ 'a8',
+ 'a9',
+ 'red',
+ 101,
+ ],
+ [
+ '1899-12-30T08:00:00.000Z',
+ '1899-12-30T08:30:00.000Z',
+ 'A song from the hearth',
+ 'load-next',
+ 'clock',
+ '',
+ 'x',
+ 'Rainbow chase',
+ 'b0',
+ '',
+ '',
+ '',
+ '',
+ 'b5',
+ '',
+ '',
+ '',
+ '',
+ '#F00',
+ 102,
+ ],
+ [],
+ ];
+
+ const importMap = {
+ worksheet: 'event schedule',
+ timeStart: 'time start',
+ timeEnd: 'time end',
+ duration: 'duration',
+ cue: 'cue',
+ title: 'title',
+ isPublic: 'public',
+ skip: 'skip',
+ note: 'notes',
+ colour: 'colour',
+ endAction: 'end action',
+ timerType: 'timer type',
+ timeWarning: 'warning time',
+ timeDanger: 'danger time',
+ custom: {},
+ };
+ const result = parseExcel(testdata, importMap);
+ expect(result.rundown.length).toBe(2);
+ expect(result.rundown.at(0).type).toBe(SupportedEvent.Block);
+ });
+
+ it('imports as events if there is no timer type column', () => {
+ const testdata = [
+ [
+ 'Time Start',
+ 'Time End',
+ 'Title',
+ 'End Action',
+ 'Public',
+ 'Skip',
+ 'Notes',
+ 'test0',
+ 'test1',
+ 'test2',
+ 'test3',
+ 'test4',
+ 'test5',
+ 'test6',
+ 'test7',
+ 'test8',
+ 'test9',
+ 'Colour',
+ 'cue',
+ ],
+ ['', '', '', '', 'x', '', 'Ballyhoo', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'red', 101],
+ [
+ '1899-12-30T08:00:00.000Z',
+ '1899-12-30T08:30:00.000Z',
+ 'A song from the hearth',
+ 'load-next',
+ '',
+ 'x',
+ 'Rainbow chase',
+ 'b0',
+ '',
+ '',
+ '',
+ '',
+ 'b5',
+ '',
+ '',
+ '',
+ '',
+ '#F00',
+ 102,
+ ],
+ [],
+ ];
+
+ const importMap = {
+ worksheet: 'event schedule',
+ timeStart: 'time start',
+ timeEnd: 'time end',
+ duration: 'duration',
+ cue: 'cue',
+ title: 'title',
+ isPublic: 'public',
+ skip: 'skip',
+ note: 'notes',
+ colour: 'colour',
+ endAction: 'end action',
+ timerType: 'timer type',
+ timeWarning: 'warning time',
+ timeDanger: 'danger time',
+ custom: {},
+ };
+ const result = parseExcel(testdata, importMap);
+ expect(result.rundown.length).toBe(2);
+ expect(result.rundown.at(0).type).toBe(SupportedEvent.Event);
+ expect((result.rundown.at(0) as OntimeEvent).timerType).toBe(TimerType.CountDown);
+ expect(result.rundown.at(1).type).toBe(SupportedEvent.Event);
+ expect((result.rundown.at(1) as OntimeEvent).timerType).toBe(TimerType.CountDown);
+ });
});
diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts
index e1cdbbaf8..aac23bcc9 100644
--- a/apps/server/src/utils/parser.ts
+++ b/apps/server/src/utils/parser.ts
@@ -18,6 +18,7 @@ import {
TimeStrategy,
CustomFields,
EventCustomFields,
+ TimerType,
} from 'ontime-types';
import xlsx from 'node-xlsx';
@@ -176,23 +177,24 @@ export const parseExcel = (excelData: unknown[][], options?: Partial)
const event: any = {};
const eventCustomFields: EventCustomFields = {};
- row.forEach((column, j) => {
+ for (let j = 0; j < row.length; j++) {
+ const column = row[j];
// 1. we check if we have set a flag for a known field
if (j === timerTypeIndex) {
if (column === 'block') {
event.type = SupportedEvent.Block;
- }
- if (column === '' || isKnownTimerType(column)) {
+ } else if (column === '' || isKnownTimerType(column)) {
event.type = SupportedEvent.Event;
event.timerType = validateTimerType(column);
+ } else {
+ // if it is not a block or a known type, we dont import it
+ return;
}
- // if it is not a block or a known type, we dont import it
- return;
} else if (j === titleIndex) {
event.title = makeString(column, '');
// if this is a block, we have nothing else to import
if (event.type === SupportedEvent.Block) {
- return;
+ continue;
}
} else if (j === timeStartIndex) {
event.timeStart = parseExcelDate(column);
@@ -225,7 +227,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial)
if (typeof column === 'string') {
// we cant deal with empty content
if (column.length === 0) {
- return;
+ continue;
}
const columnText = column.toLowerCase();
@@ -243,11 +245,15 @@ export const parseExcel = (excelData: unknown[][], options?: Partial)
// just ignore it
}
}
- });
+ }
// if any data was found in row, push to array
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
if (keysFound > 0) {
+ if (timerTypeIndex === null) {
+ event.timerType = TimerType.CountDown;
+ event.type = SupportedEvent.Event;
+ }
rundown.push({ ...event, custom: { ...eventCustomFields } });
}
});
diff --git a/apps/server/test-db/db.json b/apps/server/test-db/db.json
index 500ead203..ddf9e09be 100644
--- a/apps/server/test-db/db.json
+++ b/apps/server/test-db/db.json
@@ -317,7 +317,7 @@
"dangerThreshold": 60000,
"endMessage": ""
},
- "aliases": [
+ "urlPresets": [
{
"enabled": true,
"alias": "test",
diff --git a/demo-db/db.json b/demo-db/db.json
index 500ead203..ddf9e09be 100644
--- a/demo-db/db.json
+++ b/demo-db/db.json
@@ -317,7 +317,7 @@
"dangerThreshold": 60000,
"endMessage": ""
},
- "aliases": [
+ "urlPresets": [
{
"enabled": true,
"alias": "test",
diff --git a/e2e/tests/fixtures/test-db.json b/e2e/tests/fixtures/test-db.json
index 500ead203..ddf9e09be 100644
--- a/e2e/tests/fixtures/test-db.json
+++ b/e2e/tests/fixtures/test-db.json
@@ -317,7 +317,7 @@
"dangerThreshold": 60000,
"endMessage": ""
},
- "aliases": [
+ "urlPresets": [
{
"enabled": true,
"alias": "test",
diff --git a/test-db/db.json b/test-db/db.json
index 1dd46eb8e..676c1e2da 100644
--- a/test-db/db.json
+++ b/test-db/db.json
@@ -24,7 +24,7 @@
"dangerColor": "#ED3333",
"endMessage": ""
},
- "aliases": [],
+ "urlPresets": [],
"customFields": {},
"osc": {
"portIn": 8888,