mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
refactor: show running gap in UI
This commit is contained in:
committed by
Carlos Valente
parent
f5936e5254
commit
73db14c383
@@ -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 { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
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 {
|
import {
|
||||||
getFirstNormal,
|
getFirstNormal,
|
||||||
getLastNormal,
|
getLastNormal,
|
||||||
@@ -10,6 +18,7 @@ import {
|
|||||||
getNextNormal,
|
getNextNormal,
|
||||||
getPreviousBlockNormal,
|
getPreviousBlockNormal,
|
||||||
getPreviousNormal,
|
getPreviousNormal,
|
||||||
|
isNewLatest,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
@@ -247,11 +256,9 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
return <RundownEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />;
|
return <RundownEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let previousStart: MaybeNumber = null;
|
let lastEntry: PlayableEvent | undefined; // used by indicators
|
||||||
let previousEnd: MaybeNumber = null;
|
let thisEntry: PlayableEvent | undefined;
|
||||||
let previousEventId: string | undefined;
|
let previousEventId: string | undefined;
|
||||||
let thisStart: MaybeNumber = null;
|
|
||||||
let thisEnd: MaybeNumber = null;
|
|
||||||
let thisId = previousEventId;
|
let thisId = previousEventId;
|
||||||
|
|
||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
@@ -279,13 +286,14 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
if (isOntimeEvent(event)) {
|
if (isOntimeEvent(event)) {
|
||||||
// event indexes are 1 based in frontend
|
// event indexes are 1 based in frontend
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
previousStart = thisStart;
|
|
||||||
previousEnd = thisEnd;
|
|
||||||
previousEventId = thisId;
|
previousEventId = thisId;
|
||||||
|
lastEntry = thisEntry;
|
||||||
|
|
||||||
if (!event.skip) {
|
if (isPlayableEvent(event)) {
|
||||||
thisStart = event.timeStart;
|
// populate previous entry
|
||||||
thisEnd = event.timeEnd;
|
if (isNewLatest(event.timeStart, event.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
|
||||||
|
thisEntry = event;
|
||||||
|
}
|
||||||
thisId = eventId;
|
thisId = eventId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,8 +320,8 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
loaded={isLoaded}
|
loaded={isLoaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
previousStart={previousStart}
|
previousStart={lastEntry?.timeStart}
|
||||||
previousEnd={previousEnd}
|
previousEnd={lastEntry?.timeEnd}
|
||||||
previousEventId={previousEventId}
|
previousEventId={previousEventId}
|
||||||
playback={isLoaded ? featureData.playback : undefined}
|
playback={isLoaded ? featureData.playback : undefined}
|
||||||
isRolling={featureData.playback === Playback.Roll}
|
isRolling={featureData.playback === Playback.Roll}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
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 { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||||
@@ -32,8 +32,8 @@ interface RundownEntryProps {
|
|||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
previousStart: MaybeNumber;
|
previousStart?: number;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd?: number;
|
||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
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
|
isRolling: boolean; // we need to know even if not related to this event
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
|||||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
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 { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
@@ -36,8 +36,8 @@ interface EventBlockProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
previousStart: MaybeNumber;
|
previousStart?: number;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd?: number;
|
||||||
colour: string;
|
colour: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
|
||||||
import {
|
import {
|
||||||
calculateDuration,
|
calculateDuration,
|
||||||
checkIsNextDay,
|
checkIsNextDay,
|
||||||
@@ -19,12 +18,8 @@ export function formatDelay(timeStart: number, delay: number): string | undefine
|
|||||||
return `New start ${timeTag}`;
|
return `New start ${timeTag}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatOverlap(
|
export function formatOverlap(timeStart: number, previousStart?: number, previousEnd?: number): string | undefined {
|
||||||
previousStart: MaybeNumber,
|
const noPreviousElement = previousEnd === undefined || previousStart === undefined;
|
||||||
previousEnd: MaybeNumber,
|
|
||||||
timeStart: number,
|
|
||||||
): string | undefined {
|
|
||||||
const noPreviousElement = previousEnd === null || previousStart === null;
|
|
||||||
if (noPreviousElement) return;
|
if (noPreviousElement) return;
|
||||||
|
|
||||||
const normalisedDuration = calculateDuration(previousStart, previousEnd);
|
const normalisedDuration = calculateDuration(previousStart, previousEnd);
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
|
||||||
|
|
||||||
import { formatDelay, formatOverlap } from './EventBlock.utils';
|
import { formatDelay, formatOverlap } from './EventBlock.utils';
|
||||||
|
|
||||||
import style from './RundownIndicators.module.scss';
|
import style from './RundownIndicators.module.scss';
|
||||||
|
|
||||||
interface RundownIndicatorProps {
|
interface RundownIndicatorProps {
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
previousStart: MaybeNumber;
|
previousStart?: number;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd?: number;
|
||||||
delay: number;
|
delay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownIndicators(props: RundownIndicatorProps) {
|
export default function RundownIndicators(props: RundownIndicatorProps) {
|
||||||
const { timeStart, previousStart, previousEnd, delay } = props;
|
const { timeStart, previousStart, previousEnd, delay } = props;
|
||||||
|
|
||||||
const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart);
|
const hasOverlap = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
const hasDelay = formatDelay(timeStart, delay);
|
const hasDelay = formatDelay(timeStart, delay);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('formatOverlap()', () => {
|
|||||||
const previousStart = 0;
|
const previousStart = 0;
|
||||||
const previousEnd = 60000; // 1 min
|
const previousEnd = 60000; // 1 min
|
||||||
const timeStart = 30000; // 30 sec
|
const timeStart = 30000; // 30 sec
|
||||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
expect(result).toEqual('Overlap 30s');
|
expect(result).toEqual('Overlap 30s');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe('formatOverlap()', () => {
|
|||||||
const previousStart = 46800000; // 13:00:00
|
const previousStart = 46800000; // 13:00:00
|
||||||
const previousEnd = 48600000; // 13:30:00
|
const previousEnd = 48600000; // 13:30:00
|
||||||
const timeStart = 48300000; // 13:25:00
|
const timeStart = 48300000; // 13:25:00
|
||||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
const result = formatOverlap(timeStart, previousStart, previousEnd);
|
||||||
expect(result).toEqual('Overlap 5m');
|
expect(result).toEqual('Overlap 5m');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ describe('formatOverlap()', () => {
|
|||||||
const previousStart = 11 * MILLIS_PER_HOUR;
|
const previousStart = 11 * MILLIS_PER_HOUR;
|
||||||
const previousEnd = 12 * MILLIS_PER_HOUR;
|
const previousEnd = 12 * MILLIS_PER_HOUR;
|
||||||
const timeStart = 6 * 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)');
|
expect(result).toBe('Gap 18h (next day)');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ describe('formatOverlap()', () => {
|
|||||||
const previousStart = 17 * MILLIS_PER_HOUR;
|
const previousStart = 17 * MILLIS_PER_HOUR;
|
||||||
const previousEnd = 23 * MILLIS_PER_HOUR;
|
const previousEnd = 23 * MILLIS_PER_HOUR;
|
||||||
const timeStart = 9 * 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)');
|
expect(result).toBe('Gap 10h (next day)');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,15 +48,31 @@ describe('formatOverlap()', () => {
|
|||||||
const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00
|
const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00
|
||||||
const previousEnd = 0; // 00:00:00
|
const previousEnd = 0; // 00:00:00
|
||||||
const timeStart = 1 * MILLIS_PER_HOUR; // 01: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)');
|
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 previousStart = 23 * MILLIS_PER_HOUR;
|
||||||
const previousEnd = 1 * MILLIS_PER_HOUR;
|
const previousEnd = 1 * MILLIS_PER_HOUR;
|
||||||
const timeStart = 2 * 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');
|
expect(result).toBe('Gap 1h');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,14 +11,7 @@ import {
|
|||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
PlayableEvent,
|
PlayableEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import { generateId, insertAtIndex, reorderArray, swapEventData, getTimeFromPrevious, isNewLatest } from 'ontime-utils';
|
||||||
generateId,
|
|
||||||
insertAtIndex,
|
|
||||||
reorderArray,
|
|
||||||
swapEventData,
|
|
||||||
getTimeFromPrevious,
|
|
||||||
checkIsNextDay,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { createPatch } from '../../utils/parser.js';
|
import { createPatch } from '../../utils/parser.js';
|
||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
@@ -140,11 +133,7 @@ export function generate(
|
|||||||
currentEntry.delay = totalDelay;
|
currentEntry.delay = totalDelay;
|
||||||
|
|
||||||
// lastEntry is the event with the latest end time
|
// lastEntry is the event with the latest end time
|
||||||
if (
|
if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
|
||||||
lastEntry === null ||
|
|
||||||
currentEntry.timeEnd > lastEntry.timeEnd ||
|
|
||||||
checkIsNextDay(lastEntry.timeStart, currentEntry.timeStart, lastEntry.duration)
|
|
||||||
) {
|
|
||||||
lastEntry = currentEntry;
|
lastEntry = currentEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
|
|||||||
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
||||||
export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js';
|
export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js';
|
||||||
export { getTimeFromPrevious } from './src/date-utils/getTimeFromPrevious.js';
|
export { getTimeFromPrevious } from './src/date-utils/getTimeFromPrevious.js';
|
||||||
|
export { isNewLatest } from './src/date-utils/isNewLatest.js';
|
||||||
|
|
||||||
// feature business logic - spreadsheet import
|
// feature business logic - spreadsheet import
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user