From 73db14c3831540db274156680f64281886f36938 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 27 Aug 2024 09:39:32 +0200 Subject: [PATCH] refactor: show running gap in UI --- apps/client/src/features/rundown/Rundown.tsx | 32 ++++++++----- .../src/features/rundown/RundownEntry.tsx | 6 +-- .../rundown/event-block/EventBlock.tsx | 6 +-- .../rundown/event-block/EventBlock.utils.ts | 9 +--- .../rundown/event-block/RundownIndicators.tsx | 8 ++-- .../__tests__/EventBlock.utils.test.ts | 30 +++++++++--- .../services/rundown-service/rundownCache.ts | 15 +----- packages/utils/index.ts | 1 + .../utils/src/date-utils/isNewLatest.test.ts | 48 +++++++++++++++++++ packages/utils/src/date-utils/isNewLatest.ts | 24 ++++++++++ 10 files changed, 129 insertions(+), 50 deletions(-) create mode 100644 packages/utils/src/date-utils/isNewLatest.test.ts create mode 100644 packages/utils/src/date-utils/isNewLatest.ts diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 04745544d..25afff27c 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -2,7 +2,15 @@ import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react' import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useHotkeys } from '@mantine/hooks'; -import { isOntimeBlock, isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types'; +import { + isOntimeBlock, + isOntimeEvent, + isPlayableEvent, + PlayableEvent, + Playback, + RundownCached, + SupportedEvent, +} from 'ontime-types'; import { getFirstNormal, getLastNormal, @@ -10,6 +18,7 @@ import { getNextNormal, getPreviousBlockNormal, getPreviousNormal, + isNewLatest, } from 'ontime-utils'; import { useEventAction } from '../../common/hooks/useEventAction'; @@ -247,11 +256,9 @@ export default function Rundown({ data }: RundownProps) { return insertAtId(SupportedEvent.Event, cursor)} />; } - let previousStart: MaybeNumber = null; - let previousEnd: MaybeNumber = null; + let lastEntry: PlayableEvent | undefined; // used by indicators + let thisEntry: PlayableEvent | undefined; let previousEventId: string | undefined; - let thisStart: MaybeNumber = null; - let thisEnd: MaybeNumber = null; let thisId = previousEventId; let eventIndex = 0; @@ -279,13 +286,14 @@ export default function Rundown({ data }: RundownProps) { if (isOntimeEvent(event)) { // event indexes are 1 based in frontend eventIndex++; - previousStart = thisStart; - previousEnd = thisEnd; previousEventId = thisId; + lastEntry = thisEntry; - if (!event.skip) { - thisStart = event.timeStart; - thisEnd = event.timeEnd; + if (isPlayableEvent(event)) { + // populate previous entry + if (isNewLatest(event.timeStart, event.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) { + thisEntry = event; + } thisId = eventId; } } @@ -312,8 +320,8 @@ export default function Rundown({ data }: RundownProps) { loaded={isLoaded} hasCursor={hasCursor} isNext={isNext} - previousStart={previousStart} - previousEnd={previousEnd} + previousStart={lastEntry?.timeStart} + previousEnd={lastEntry?.timeEnd} previousEventId={previousEventId} playback={isLoaded ? featureData.playback : undefined} isRolling={featureData.playback === Playback.Roll} diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 084a0fac9..163129916 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { MaybeNumber, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; +import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; import { useEventAction } from '../../common/hooks/useEventAction'; import useMemoisedFn from '../../common/hooks/useMemoisedFn'; @@ -32,8 +32,8 @@ interface RundownEntryProps { eventIndex: number; hasCursor: boolean; isNext: boolean; - previousStart: MaybeNumber; - previousEnd: MaybeNumber; + previousStart?: number; + previousEnd?: number; previousEventId?: string; playback?: Playback; // we only care about this if this event is playing isRolling: boolean; // we need to know even if not related to this event diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index c26ac7364..96cf546c5 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -8,7 +8,7 @@ import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; -import { EndAction, MaybeNumber, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types'; +import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; @@ -36,8 +36,8 @@ interface EventBlockProps { title: string; note: string; delay: number; - previousStart: MaybeNumber; - previousEnd: MaybeNumber; + previousStart?: number; + previousEnd?: number; colour: string; isPast: boolean; isNext: boolean; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.utils.ts b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts index 9e3f8fd9a..787be6fda 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.utils.ts +++ b/apps/client/src/features/rundown/event-block/EventBlock.utils.ts @@ -1,4 +1,3 @@ -import { MaybeNumber } from 'ontime-types'; import { calculateDuration, checkIsNextDay, @@ -19,12 +18,8 @@ export function formatDelay(timeStart: number, delay: number): string | undefine return `New start ${timeTag}`; } -export function formatOverlap( - previousStart: MaybeNumber, - previousEnd: MaybeNumber, - timeStart: number, -): string | undefined { - const noPreviousElement = previousEnd === null || previousStart === null; +export function formatOverlap(timeStart: number, previousStart?: number, previousEnd?: number): string | undefined { + const noPreviousElement = previousEnd === undefined || previousStart === undefined; if (noPreviousElement) return; const normalisedDuration = calculateDuration(previousStart, previousEnd); diff --git a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx index 9cfa7afae..82a34e336 100644 --- a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx +++ b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx @@ -1,20 +1,18 @@ -import { MaybeNumber } from 'ontime-types'; - import { formatDelay, formatOverlap } from './EventBlock.utils'; import style from './RundownIndicators.module.scss'; interface RundownIndicatorProps { timeStart: number; - previousStart: MaybeNumber; - previousEnd: MaybeNumber; + previousStart?: number; + previousEnd?: number; delay: number; } export default function RundownIndicators(props: RundownIndicatorProps) { const { timeStart, previousStart, previousEnd, delay } = props; - const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart); + const hasOverlap = formatOverlap(timeStart, previousStart, previousEnd); 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 index 2ade46bf8..544ff8506 100644 --- 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 @@ -16,7 +16,7 @@ describe('formatOverlap()', () => { const previousStart = 0; const previousEnd = 60000; // 1 min const timeStart = 30000; // 30 sec - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toEqual('Overlap 30s'); }); @@ -24,7 +24,7 @@ describe('formatOverlap()', () => { const previousStart = 46800000; // 13:00:00 const previousEnd = 48600000; // 13:30:00 const timeStart = 48300000; // 13:25:00 - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toEqual('Overlap 5m'); }); @@ -32,7 +32,7 @@ describe('formatOverlap()', () => { const previousStart = 11 * MILLIS_PER_HOUR; const previousEnd = 12 * MILLIS_PER_HOUR; const timeStart = 6 * MILLIS_PER_HOUR; - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toBe('Gap 18h (next day)'); }); @@ -40,7 +40,7 @@ describe('formatOverlap()', () => { const previousStart = 17 * MILLIS_PER_HOUR; const previousEnd = 23 * MILLIS_PER_HOUR; const timeStart = 9 * MILLIS_PER_HOUR; - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toBe('Gap 10h (next day)'); }); @@ -48,15 +48,31 @@ describe('formatOverlap()', () => { const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00 const previousEnd = 0; // 00:00:00 const timeStart = 1 * MILLIS_PER_HOUR; // 01:00:00 - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toBe('Gap 1h (next day)'); }); - it('handles events the day after, with previous ending over midnight', () => { + it('handles sequential events the day after, with previous ending over midnight', () => { + const previousStart = 23 * MILLIS_PER_HOUR; + const previousEnd = 1 * MILLIS_PER_HOUR; + const timeStart = 1 * MILLIS_PER_HOUR; + const result = formatOverlap(timeStart, previousStart, previousEnd); + expect(result).toBeUndefined(); + }); + + it('handles events the day after, with previous ending over midnight with overlap', () => { + const previousStart = 23 * MILLIS_PER_HOUR; + const previousEnd = 2 * MILLIS_PER_HOUR; + const timeStart = 1 * MILLIS_PER_HOUR; + const result = formatOverlap(timeStart, previousStart, previousEnd); + expect(result).toBe('Overlap 1h'); + }); + + it('handles events the day after, with previous ending over midnight with gap', () => { const previousStart = 23 * MILLIS_PER_HOUR; const previousEnd = 1 * MILLIS_PER_HOUR; const timeStart = 2 * MILLIS_PER_HOUR; - const result = formatOverlap(previousStart, previousEnd, timeStart); + const result = formatOverlap(timeStart, previousStart, previousEnd); expect(result).toBe('Gap 1h'); }); }); diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 77381382e..7583302c8 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -11,14 +11,7 @@ import { OntimeRundownEntry, PlayableEvent, } from 'ontime-types'; -import { - generateId, - insertAtIndex, - reorderArray, - swapEventData, - getTimeFromPrevious, - checkIsNextDay, -} from 'ontime-utils'; +import { generateId, insertAtIndex, reorderArray, swapEventData, getTimeFromPrevious, isNewLatest } from 'ontime-utils'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { createPatch } from '../../utils/parser.js'; import { apply } from './delayUtils.js'; @@ -140,11 +133,7 @@ export function generate( currentEntry.delay = totalDelay; // lastEntry is the event with the latest end time - if ( - lastEntry === null || - currentEntry.timeEnd > lastEntry.timeEnd || - checkIsNextDay(lastEntry.timeStart, currentEntry.timeStart, lastEntry.duration) - ) { + if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) { lastEntry = currentEntry; } } diff --git a/packages/utils/index.ts b/packages/utils/index.ts index a2ec92a35..47fe5d351 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -76,6 +76,7 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali export { checkIsNow } from './src/date-utils/checkIsNow.js'; export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js'; export { getTimeFromPrevious } from './src/date-utils/getTimeFromPrevious.js'; +export { isNewLatest } from './src/date-utils/isNewLatest.js'; // feature business logic - spreadsheet import export { diff --git a/packages/utils/src/date-utils/isNewLatest.test.ts b/packages/utils/src/date-utils/isNewLatest.test.ts new file mode 100644 index 000000000..c7809d6a6 --- /dev/null +++ b/packages/utils/src/date-utils/isNewLatest.test.ts @@ -0,0 +1,48 @@ +import { MILLIS_PER_HOUR } from './conversionUtils'; +import { isNewLatest } from './isNewLatest'; + +describe('isNewLatest', () => { + it('should be true if there is no previous', () => { + expect(isNewLatest(0, 60000)).toBeTruthy(); + }); + + it('should be true if it starts when the previous finishes', () => { + const nowStart = 10 * MILLIS_PER_HOUR; + const nowEnd = 11 * MILLIS_PER_HOUR; + const previousStart = 9 * MILLIS_PER_HOUR; + const previousEnd = 10 * MILLIS_PER_HOUR; + expect(isNewLatest(nowStart, nowEnd, previousStart, previousEnd)).toBeTruthy(); + }); + + it('should be true if it starts the same day the previous finishes', () => { + const nowStart = 22 * MILLIS_PER_HOUR; + const nowEnd = 23 * MILLIS_PER_HOUR; + const previousStart = 9 * MILLIS_PER_HOUR; + const previousEnd = 20 * MILLIS_PER_HOUR; + expect(isNewLatest(nowStart, nowEnd, previousStart, previousEnd)).toBeTruthy(); + }); + + it('should be true if it finishes after the previous, accounting for passing midnight', () => { + const nowStart = 1 * MILLIS_PER_HOUR; + const nowEnd = 3 * MILLIS_PER_HOUR; + const previousStart = 23 * MILLIS_PER_HOUR; + const previousEnd = 2 * MILLIS_PER_HOUR; + expect(isNewLatest(nowStart, nowEnd, previousStart, previousEnd)).toBeTruthy(); + }); + + it('should be true if it the next day', () => { + const nowStart = 8 * MILLIS_PER_HOUR; + const nowEnd = 10 * MILLIS_PER_HOUR; + const previousStart = 9 * MILLIS_PER_HOUR; + const previousEnd = 11 * MILLIS_PER_HOUR; + expect(isNewLatest(nowStart, nowEnd, previousStart, previousEnd)).toBeTruthy(); + }); + + it('should be true if it the next day (2)', () => { + const nowStart = 9 * MILLIS_PER_HOUR; + const nowEnd = 11 * MILLIS_PER_HOUR; + const previousStart = 9 * MILLIS_PER_HOUR; + const previousEnd = 11 * MILLIS_PER_HOUR; + expect(isNewLatest(nowStart, nowEnd, previousStart, previousEnd)).toBeTruthy(); + }); +}); diff --git a/packages/utils/src/date-utils/isNewLatest.ts b/packages/utils/src/date-utils/isNewLatest.ts new file mode 100644 index 000000000..3e86e1a9a --- /dev/null +++ b/packages/utils/src/date-utils/isNewLatest.ts @@ -0,0 +1,24 @@ +import { checkIsNextDay } from './checkIsNextDay.js'; + +/** + * Checks whether a new element is the latest in the list + */ +export function isNewLatest(timeStart: number, timeEnd: number, previousStart?: number, previousEnd?: number): boolean { + // true if there is no previous + if (previousStart === undefined || previousEnd === undefined) { + return true; + } + + // true if it starts after the previous is finished + if (timeStart >= previousEnd) { + return true; + } + + // true if it finishes later than previous + if (timeEnd > previousEnd) { + return true; + } + + // true if it is the day after + return checkIsNextDay(previousStart, timeStart, previousEnd - previousStart); +}