diff --git a/apps/client/package.json b/apps/client/package.json index 762b2ad3f..a53838cd9 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -60,7 +60,7 @@ ] }, "devDependencies": { - "@sentry/vite-plugin": "^2.10.2", + "@sentry/vite-plugin": "^2.14.0", "@tanstack/eslint-plugin-query": "^5.8.4", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.1.1", @@ -86,10 +86,10 @@ "prettier": "^3.0.3", "sass": "^1.57.1", "typescript": "^5.2.2", - "vite": "^5.0.10", - "vite-plugin-compression2": "^0.11.0", + "vite": "^5.1.0", + "vite-plugin-compression2": "^0.12.0", "vite-plugin-svgr": "^4.2.0", - "vite-tsconfig-paths": "^4.2.2", - "vitest": "^1.0.4" + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.2" } } diff --git a/apps/client/src/common/components/delay-indicator/DelayIndicator.tsx b/apps/client/src/common/components/delay-indicator/DelayIndicator.tsx index 14b01a55d..f1a4df211 100644 --- a/apps/client/src/common/components/delay-indicator/DelayIndicator.tsx +++ b/apps/client/src/common/components/delay-indicator/DelayIndicator.tsx @@ -17,7 +17,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) { if (typeof delayValue === 'number') { if (delayValue < 0) { return ( - + @@ -27,7 +27,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) { if (delayValue > 0) { return ( - + diff --git a/apps/client/src/common/components/input/time-input/TimeInput.module.scss b/apps/client/src/common/components/input/time-input/TimeInput.module.scss index 8f7de79d5..1a30b2cd6 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.module.scss +++ b/apps/client/src/common/components/input/time-input/TimeInput.module.scss @@ -1,8 +1,4 @@ -$input-font-size: 15px; -$input-delayed-border-color: #E69056; - .timeInput { - font-size: $input-font-size; letter-spacing: 1px; width: 6.5em; -} \ No newline at end of file +} diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index e70ae8f2c..b9de8e013 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -4,6 +4,7 @@ import { millisToString } from 'ontime-utils'; import { useEmitLog } from '../../../stores/logger'; import { forgivingStringToMillis } from '../../../utils/dateConfig'; +import { cx } from '../../../utils/styleUtils'; import style from './TimeInput.module.scss'; interface TimeInputProps { @@ -11,11 +12,12 @@ interface TimeInputProps { submitHandler: (field: T, value: string) => void; time?: number; placeholder: string; + disabled?: boolean; className?: string; } export default function TimeInput(props: TimeInputProps) { - const { name, submitHandler, time = 0, placeholder, className } = props; + const { name, submitHandler, time = 0, placeholder, disabled, className } = props; const { emitError } = useEmitLog(); const inputRef = useRef(null); const [value, setValue] = useState(''); @@ -118,14 +120,16 @@ export default function TimeInput(props: TimeInputProps) { resetValue(); }, [resetValue, time]); - const timeInputClass = className ? className : style.timeInput; + const timeInputClasses = cx([style.timeInput, className]); return ( { submitHandler: (field: T, value: string) => void; time?: number; hasDelay?: boolean; + disabled?: boolean; placeholder: string; } -export default function TimeInputWithButton(props: TimeInputWithButtonProps) { - const { name, submitHandler, time, hasDelay, placeholder } = props; +export default function TimeInputWithButton(props: PropsWithChildren>) { + const { name, submitHandler, time, hasDelay, placeholder, disabled, children } = props; const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]); return ( - - - - - - - name={name} - submitHandler={submitHandler} - time={time} - placeholder={placeholder} - className={style.inputField} - /> + + + name={name} + submitHandler={submitHandler} + time={time} + placeholder={placeholder} + className={style.inputField} + disabled={disabled} + /> + + {children} ); } diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index d9c818e2c..1f300131e 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types'; -import { reorderArray, swapEventData } from 'ontime-utils'; +import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types'; +import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils'; import { RUNDOWN } from '../api/apiConstants'; import { logAxiosError } from '../api/apiUtils'; @@ -186,6 +186,7 @@ export const useEventAction = () => { // check for adding time keyword } else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) { + // TODO: is this logic solid? const remainingString = value.substring(1); newValMillis = getPreviousEnd() + forgivingStringToMillis(remainingString); } else { @@ -205,6 +206,44 @@ export const useEventAction = () => { [_updateEventMutation, queryClient], ); + /** + * Toggles link of an event to the previous + */ + const linkTimer = useCallback( + async (eventId: string, linkStart: MaybeString) => { + let newEvent: Partial = { id: eventId }; + + if (!linkStart) { + newEvent.linkStart = null; + } else { + const cachedRundown = queryClient.getQueryData(RUNDOWN); + if (!cachedRundown) { + return; + } + const currentEvent = cachedRundown.rundown[eventId] as OntimeEvent; + if (!isOntimeEvent(currentEvent)) { + return; + } + const { previousEvent } = getPreviousEventNormal(cachedRundown.rundown, cachedRundown.order, eventId); + + if (!previousEvent) { + newEvent.linkStart = null; + } else { + newEvent.linkStart = previousEvent.id; + const timePatch = getLinkedTimes(currentEvent, previousEvent); + newEvent = { ...newEvent, ...timePatch }; + } + } + + try { + await _updateEventMutation.mutateAsync(newEvent); + } catch (error) { + logAxiosError('Error updating event', error); + } + }, + [_updateEventMutation, queryClient], + ); + /** * Calls mutation to edit multiple events * @private @@ -509,6 +548,7 @@ export const useEventAction = () => { batchUpdateEvents, deleteEvent, deleteAllEvents, + linkTimer, reorderEvent, swapEvents, updateEvent, diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 20d262593..a378b4f2f 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -1,7 +1,4 @@ -import { - forgivingStringToMillis, - millisToDelayString, -} from '../dateConfig'; +import { forgivingStringToMillis, millisToDelayString } from '../dateConfig'; describe('test forgivingStringToMillis()', () => { describe('function handles time with no separators', () => { @@ -270,10 +267,10 @@ describe('millisToDelayString()', () => { }); describe('converts values in seconds', () => { it('shows a simple string with value in seconds', () => { - expect(millisToDelayString(10000)).toBe('+10 sec'); + expect(millisToDelayString(10000, true)).toBe('+10 sec'); }); it('... and its negative counterpart', () => { - expect(millisToDelayString(-10000)).toBe('-10 sec'); + expect(millisToDelayString(-10000, true)).toBe('-10 sec'); }); const underAMinute = [1, 500, 1000, 6000, 55000, 59999]; @@ -287,32 +284,32 @@ describe('millisToDelayString()', () => { describe('converts values in minutes', () => { it('shows a simple string with value in minutes', () => { - expect(millisToDelayString(720000)).toBe('+12 min'); + expect(millisToDelayString(720000, true)).toBe('+12 min'); }); it('... and its negative counterpart', () => { - expect(millisToDelayString(-720000)).toBe('-12 min'); + expect(millisToDelayString(-720000, true)).toBe('-12 min'); }); it('shows a simple string with value in minutes and seconds', () => { - expect(millisToDelayString(630000)).toBe('+00:10:30'); + expect(millisToDelayString(630000, true)).toBe('+00:10:30'); }); it('... and its negative counterpart', () => { - expect(millisToDelayString(-630000)).toBe('-00:10:30'); + expect(millisToDelayString(-630000, true)).toBe('-00:10:30'); }); const underAnHour = [60000, 360000, 720000]; underAnHour.forEach((value) => { it(`handles ${value}`, () => { - expect(millisToDelayString(value)?.endsWith('min')).toBe(true); + expect(millisToDelayString(value, true)?.endsWith('min')).toBe(true); }); }); }); describe('converts values with full time string', () => { it('positive added time', () => { - expect(millisToDelayString(45015000)).toBe('+12:30:15'); + expect(millisToDelayString(45015000, true)).toBe('+12:30:15'); }); it('negative added time', () => { - expect(millisToDelayString(-45015000)).toBe('-12:30:15'); + expect(millisToDelayString(-45015000, true)).toBe('-12:30:15'); }); }); }); diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 7ddb22c52..bf4cfd58d 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -155,19 +155,21 @@ export const forgivingStringToMillis = (value: string): number => { return millis; }; -export function millisToDelayString(millis: number | null): undefined | string | null { +export function millisToDelayString(millis: number | null, small = false): undefined | string | null { if (millis == null || millis === 0) { return null; } const isNegative = millis < 0; const absMillis = Math.abs(millis); + const delayed = small ? '+' : 'delayed by '; + const ahead = small ? '-' : 'ahead by '; if (absMillis < MILLIS_PER_MINUTE) { - return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')} sec`; + return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 's')} sec`; } else if (absMillis < MILLIS_PER_HOUR && absMillis % MILLIS_PER_MINUTE === 0) { - return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')} min`; + return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'm')} min`; } else { - return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`; + return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'HH:mm:ss')}`; } } diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index f41b8988a..4d8228889 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -21,6 +21,8 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { duration: event.duration, timeEnd: event.timeEnd, timerType: event.timerType, + timeStrategy: event.timeStrategy, + linkStart: event.linkStart, endAction: event.endAction, isPublic: event.isPublic, skip: event.skip, diff --git a/apps/client/src/features/cuesheet/Cuesheet.module.scss b/apps/client/src/features/cuesheet/Cuesheet.module.scss index 2a6a3ae83..3c52b9a22 100644 --- a/apps/client/src/features/cuesheet/Cuesheet.module.scss +++ b/apps/client/src/features/cuesheet/Cuesheet.module.scss @@ -25,7 +25,8 @@ $table-header-font-size: calc(1rem - 3px); display: flex; } - th, td { + th, + td { margin: 1px; font-weight: inherit; font-size: inherit; @@ -102,6 +103,9 @@ $table-header-font-size: calc(1rem - 3px); position: sticky; left: 47.5%; // center of the screen, ish padding: 0.5rem 0; + &:first-letter { + text-transform: uppercase; + } } } diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 127fa2895..f2cd03b8b 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -126,6 +126,8 @@ export default function RundownEntry(props: RundownEntryProps) { timeStart={data.timeStart} timeEnd={data.timeEnd} duration={data.duration} + timeStrategy={data.timeStrategy} + linkStart={data.linkStart} eventId={data.id} isPublic={data.isPublic} endAction={data.endAction} diff --git a/apps/client/src/features/rundown/event-block/EventBlock.module.scss b/apps/client/src/features/rundown/event-block/EventBlock.module.scss index 1968f3d8a..65890902d 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.module.scss +++ b/apps/client/src/features/rundown/event-block/EventBlock.module.scss @@ -125,12 +125,6 @@ $skip-opacity: 0.1; display: flex; gap: $block-clearance; height: 100%; - - .timerNote { - color: $blue-500; - margin-right: $block-clearance; - font-size: 1.5em; - } } .eventTitle { @@ -143,6 +137,7 @@ $skip-opacity: 0.1; .eventActions { grid-area: actions; height: 100%; + text-align: end; } .progressBg { diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index a4baa934e..a23e6455c 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -7,7 +7,7 @@ import { IoPeople } from '@react-icons/all-files/io5/IoPeople'; 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 { EndAction, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types'; +import { EndAction, MaybeNumber, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import copyToClipboard from '../../../common/utils/copyToClipboard'; @@ -26,6 +26,8 @@ interface EventBlockProps { timeStart: number; timeEnd: number; duration: number; + timeStrategy: TimeStrategy; + linkStart: MaybeString; eventId: string; eventIndex: number; isPublic: boolean; @@ -61,6 +63,8 @@ export default function EventBlock(props: EventBlockProps) { timeStart, timeEnd, duration, + timeStrategy, + linkStart, isPublic = true, eventIndex, endAction, @@ -251,6 +255,8 @@ export default function EventBlock(props: EventBlockProps) { timeStart={timeStart} timeEnd={timeEnd} duration={duration} + linkStart={linkStart} + timeStrategy={timeStrategy} eventId={eventId} eventIndex={eventIndex} isPublic={isPublic} diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index 53206d608..27da5edf9 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -9,16 +9,16 @@ import { IoPlayForward } from '@react-icons/all-files/io5/IoPlayForward'; import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward'; import { IoStop } from '@react-icons/all-files/io5/IoStop'; import { IoTime } from '@react-icons/all-files/io5/IoTime'; -import { EndAction, Playback, TimerType } from 'ontime-types'; +import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types'; import { tooltipDelayMid } from '../../../ontimeConfig'; import EditableBlockTitle from '../common/EditableBlockTitle'; import { EventItemActions } from '../RundownEntry'; +import TimeInputFlow from '../time-input-flow/TimeInputFlow'; import BlockActionMenu from './composite/BlockActionMenu'; import EventBlockPlayback from './composite/EventBlockPlayback'; import EventBlockProgressBar from './composite/EventBlockProgressBar'; -import EventBlockTimers from './composite/EventBlockTimers'; import style from './EventBlock.module.scss'; @@ -30,6 +30,8 @@ interface EventBlockInnerProps { timeStart: number; timeEnd: number; duration: number; + timeStrategy: TimeStrategy; + linkStart: MaybeString; eventId: string; eventIndex: number; isPublic: boolean; @@ -51,6 +53,8 @@ const EventBlockInner = (props: EventBlockInnerProps) => { timeStart, timeEnd, duration, + timeStrategy, + linkStart, eventId, isPublic = true, endAction, @@ -84,7 +88,17 @@ const EventBlockInner = (props: EventBlockInnerProps) => { return !renderInner ? null : ( <> - +
+ +
{next && ( @@ -99,7 +113,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { selected={selected} disablePlayback={skip || isRolling} /> -
+
{note}
{selected && } @@ -117,10 +131,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { - +
diff --git a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx index e87e9f379..b557231ba 100644 --- a/apps/client/src/features/rundown/event-block/RundownIndicators.tsx +++ b/apps/client/src/features/rundown/event-block/RundownIndicators.tsx @@ -13,7 +13,7 @@ function formatDelay(timeStart: number, delay: number): string | undefined { const delayedStart = Math.max(0, timeStart + delay); const timeTag = removeTrailingZero(millisToString(delayedStart)); - return `New start: ${timeTag}`; + return `New start ${timeTag}`; } function formatOverlap(previousEnd: number | null, timeStart: number): string | undefined { diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx deleted file mode 100644 index 0ee705d08..000000000 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { memo } from 'react'; -import { Tooltip } from '@chakra-ui/react'; -import { IoAlertCircleOutline } from '@react-icons/all-files/io5/IoAlertCircleOutline'; -import { OntimeEvent } from 'ontime-types'; - -import TimeInputWithButton from '../../../../common/components/input/time-input/TimeInputWithButton'; -import { useEventAction } from '../../../../common/hooks/useEventAction'; -import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; -import { tooltipDelayFast } from '../../../../ontimeConfig'; - -import style from '../EventBlock.module.scss'; - -interface EventBlockTimerProps { - eventId: string; - timeStart: number; - timeEnd: number; - duration: number; - delay: number; -} - -type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride'; // we call it durationOverride to stop from passing as a duration value - -const EventBlockTimers = (props: EventBlockTimerProps) => { - const { eventId, timeStart, timeEnd, duration, delay } = props; - const { updateEvent, updateTimer } = useEventAction(); - - // In sync with EventEditorTimes - const handleSubmit = (field: TimeActions, value: string) => { - if (field === 'timeStart' || field === 'timeEnd') { - updateTimer(eventId, field, value); - return; - } - - if (field === 'durationOverride') { - const timeInMillis = forgivingStringToMillis(value); - const newEventData: Partial = { id: eventId, timeEnd: timeStart + timeInMillis }; - updateEvent(newEventData); - return; - } - }; - - const overMidnight = timeStart > timeEnd; - const hasDelay = delay !== 0; - - return ( -
- - name='timeStart' - submitHandler={handleSubmit} - time={timeStart} - hasDelay={hasDelay} - placeholder='Start' - /> - - name='timeEnd' - submitHandler={handleSubmit} - time={timeEnd} - hasDelay={hasDelay} - placeholder='End' - /> - - name='durationOverride' - submitHandler={handleSubmit} - time={duration} - placeholder='Duration' - /> - {overMidnight && ( -
- - - -
- )} -
- ); -}; - -export default memo(EventBlockTimers); 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 6d7bcb9f1..a52c53a97 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss @@ -17,7 +17,6 @@ flex-direction: column; gap: 2rem; overflow-y: auto; - } .footer { @@ -45,9 +44,15 @@ font-size: calc(1rem - 3px); color: $label-gray; margin-bottom: 0.25rem; +} - &.delayLabel { - color: $ontime-delay-text; +.delayLabel { + font-size: calc(1rem - 3px); + color: $ontime-delay-text; + margin-top: 0.25rem; + + &::after { + content: '\200b'; } } @@ -55,8 +60,7 @@ font-size: calc(1rem - 3px); color: $label-gray; display: flex; - align-items: center; - gap: 0.25rem; + gap: 0.5rem; max-width: max-content; cursor: pointer; } diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.tsx b/apps/client/src/features/rundown/event-editor/EventEditor.tsx index b81328117..7f1fb99c8 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/event-editor/EventEditor.tsx @@ -100,6 +100,8 @@ export default function EventEditor() { timeStart={event.timeStart} timeEnd={event.timeEnd} duration={event.duration} + timeStrategy={event.timeStrategy} + linkStart={event.linkStart} 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 176ddfa57..0c27d99d9 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx @@ -1,13 +1,12 @@ import { memo } from 'react'; import { Select, Switch } from '@chakra-ui/react'; -import { EndAction, OntimeEvent, TimerType } from 'ontime-types'; +import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types'; import { millisToString } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; -import TimeInputWithButton from '../../../../common/components/input/time-input/TimeInputWithButton'; import { useEventAction } from '../../../../common/hooks/useEventAction'; import { forgivingStringToMillis, millisToDelayString } from '../../../../common/utils/dateConfig'; -import { cx } from '../../../../common/utils/styleUtils'; +import TimeInputFlow from '../../time-input-flow/TimeInputFlow'; import style from '../EventEditor.module.scss'; @@ -16,6 +15,8 @@ interface EventEditorTimesProps { timeStart: number; timeEnd: number; duration: number; + timeStrategy: TimeStrategy; + linkStart: MaybeString; delay: number; isPublic: boolean; endAction: EndAction; @@ -25,27 +26,23 @@ interface EventEditorTimesProps { } type HandledActions = 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger'; -type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride'; // we call it durationOverride to stop from passing as a duration value const EventEditorTimes = (props: EventEditorTimesProps) => { - const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType, timeWarning, timeDanger } = - props; - const { updateEvent, updateTimer } = useEventAction(); - - // In sync with EventBlockTimers - const handleTimeSubmit = (field: TimeActions, value: string) => { - if (field === 'timeStart' || field === 'timeEnd') { - updateTimer(eventId, field, value); - return; - } - - if (field === 'durationOverride') { - const timeInMillis = forgivingStringToMillis(value); - const newEventData: Partial = { id: eventId, timeEnd: timeStart + timeInMillis }; - updateEvent(newEventData); - return; - } - }; + const { + eventId, + timeStart, + timeEnd, + duration, + timeStrategy, + linkStart, + delay, + isPublic, + endAction, + timerType, + timeWarning, + timeDanger, + } = props; + const { updateEvent } = useEventAction(); const handleSubmit = (field: HandledActions, value: string | boolean) => { if (field === 'isPublic') { @@ -66,49 +63,28 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { }; const hasDelay = delay !== 0; - const delayTime = hasDelay ? millisToDelayString(delay) : null; - const startLabel = delayTime ? `New Start ${millisToString(timeStart + delay)}` : 'Start time'; - const endLabel = delayTime ? `New End ${millisToString(timeEnd + delay)}` : 'End time'; - const inputTimeLabels = cx([style.inputLabel, hasDelay ? style.delayLabel : null]); + const delayLabel = hasDelay + ? `Event is ${millisToDelayString(delay)}. New schedule ${millisToString(timeStart + delay)} → ${millisToString( + timeEnd + delay, + )}` + : ''; return ( -
-
-
- - -
-
- - -
-
- - +
+
Event schedule
+
+
+
{delayLabel}
@@ -159,11 +135,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
Event Visibility
-
+ ); }; diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss new file mode 100644 index 000000000..e4359d972 --- /dev/null +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss @@ -0,0 +1,29 @@ +.timeLabel { + position: absolute; + z-index: 100; + font-size: 0.75rem; + font-weight: 600; + top: 2px; + right: 4px; + color: inherit; +} + +.timeAction { + opacity: 0.4; + cursor: pointer; + padding-right: 0.5em; + + &.active { + opacity: 1; + color: var(--status-color-active-override, $active-indicator); + } + .fourtyfive { + transform: rotate(-45deg); + } +} + +.timerNote { + color: $blue-500; + margin-right: 0.5rem; + font-size: 1.5em; +} diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx new file mode 100644 index 000000000..472f4ff40 --- /dev/null +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx @@ -0,0 +1,126 @@ +import { memo } from 'react'; +import { InputRightElement, Tooltip } from '@chakra-ui/react'; +import { IoAlertCircleOutline } from '@react-icons/all-files/io5/IoAlertCircleOutline'; +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, OntimeEvent, TimeStrategy } from 'ontime-types'; + +import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton'; +import { useEventAction } from '../../../common/hooks/useEventAction'; +import { cx } from '../../../common/utils/styleUtils'; +import { tooltipDelayFast } from '../../../ontimeConfig'; + +import style from './TimeInputFlow.module.scss'; + +interface EventBlockTimerProps { + eventId: string; + timeStart: number; + timeEnd: number; + duration: number; + timeStrategy: TimeStrategy; + linkStart: MaybeString; + delay: number; +} + +type TimeActions = 'timeStart' | 'timeEnd' | 'duration'; + +const TimeInputFlow = (props: EventBlockTimerProps) => { + const { eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, delay } = props; + const { updateEvent, updateTimer, linkTimer } = useEventAction(); + + // In sync with EventEditorTimes + const handleSubmit = (field: TimeActions, value: string) => { + updateTimer(eventId, field, value); + }; + + const handleChangeStrategy = (timeStrategy: TimeStrategy) => { + const newEvent: Partial = { id: eventId, timeStrategy }; + updateEvent(newEvent); + }; + + const handleLink = (doLink: boolean) => { + // the string doesnt mean much for now, not more than an intent to link + // we imagine that we can leverage this to create offsets p+10 + linkTimer(eventId, doLink ? 'p' : null); + }; + + const overMidnight = timeStart > timeEnd; + const hasDelay = delay !== 0; + + const isLockedEnd = timeStrategy === TimeStrategy.LockEnd; + const isLockedDuration = timeStrategy === TimeStrategy.LockDuration; + + const activeStart = cx([style.timeAction, linkStart ? style.active : null]); + const activeEnd = cx([style.timeAction, isLockedEnd ? style.active : null]); + const activeDuration = cx([style.timeAction, isLockedDuration ? style.active : null]); + + return ( + <> + + name='timeStart' + submitHandler={handleSubmit} + time={timeStart} + hasDelay={hasDelay} + placeholder='Start' + disabled={Boolean(linkStart)} + > + handleLink(!linkStart)}> + S + {linkStart ? : } + +
+ + + name='timeEnd' + submitHandler={handleSubmit} + time={timeEnd} + hasDelay={hasDelay} + disabled={isLockedDuration} + placeholder='End' + > + handleChangeStrategy(TimeStrategy.LockEnd)} + data-testid='lock__end' + > + E + {isLockedEnd ? : } + + + + + name='duration' + submitHandler={handleSubmit} + time={duration} + disabled={isLockedEnd} + placeholder='Duration' + > + handleChangeStrategy(TimeStrategy.LockDuration)} + data-testid='lock__duration' + > + D + {isLockedDuration ? : } + + + + {overMidnight && ( +
+ + + +
+ )} + + ); +}; + +export default memo(TimeInputFlow); diff --git a/apps/client/src/index.scss b/apps/client/src/index.scss index 766d6ea08..3e4ee9c74 100644 --- a/apps/client/src/index.scss +++ b/apps/client/src/index.scss @@ -7,7 +7,7 @@ $thumb-color-hover: $gray-900; * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - scrollbar-color: $track-color $thumb-color; + scrollbar-color: $thumb-color $track-color; scrollbar-width: thin; } @@ -48,7 +48,7 @@ option { /* Track */ ::-webkit-scrollbar-track { - background: $track-color; + background: $white-1; border-radius: 2px; } diff --git a/apps/server/package.json b/apps/server/package.json index ced038929..5c4daad31 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -45,7 +45,7 @@ "shx": "^0.3.4", "ts-node": "^10.9.1", "typescript": "^5.2.2", - "vitest": "^1.0.4" + "vitest": "^1.2.2" }, "scripts": { "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index c351f3900..cc5128623 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -12,6 +12,7 @@ import { currentDirectory, environment, isProduction, + resolveDbPath, resolveExternalsDirectory, resolveStylesDirectory, resolvedPath, @@ -42,13 +43,14 @@ import { restoreService } from './services/RestoreService.js'; import { messageService } from './services/message-service/MessageService.js'; import { populateDemo } from './modules/loadDemo.js'; import { getState, updateNumEvents } from './stores/runtimeState.js'; -import { getNumEvents } from './services/rundown-service/RundownService.js'; +import { getNumEvents, setRundown } from './services/rundown-service/RundownService.js'; console.log(`Starting Ontime version ${ONTIME_VERSION}`); if (!isProduction) { console.log(`Ontime running in ${environment} environment`); console.log(`Ontime directory at ${currentDirectory} `); + console.log(`Ontime database at ${resolveDbPath}`); } // Create express APP @@ -177,16 +179,20 @@ export const startServer = async () => { }, }); + // initialise rundown service + const persistedRundown = DataProvider.getRundown(); + setRundown(persistedRundown); + + // TODO: do this on the init of the runtime service + const numEvents = getNumEvents(); + updateNumEvents(numEvents); + // load restore point if it exists const maybeRestorePoint = await restoreService.load(); // TODO: pass event store to rundownservice runtimeService.init(maybeRestorePoint); - // TODO: do this on the init of the runtime service - const numEvents = getNumEvents(); - updateNumEvents(numEvents); - // eventStore set is a dependency of the services that publish to it messageService.init(eventStore.set.bind(eventStore)); @@ -276,7 +282,7 @@ export const shutdown = async (exitCode = 0) => { process.exit(exitCode); }; -process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`)); +process.on('exit', (code) => console.log(`Ontime shutdown with code: ${code}`)); process.on('unhandledRejection', async (error) => { logger.error(LogOrigin.Server, `Error: unhandled rejection ${error}`); diff --git a/apps/server/src/controllers/rundownController.ts b/apps/server/src/controllers/rundownController.ts index b514dea4f..f0706fe7c 100644 --- a/apps/server/src/controllers/rundownController.ts +++ b/apps/server/src/controllers/rundownController.ts @@ -10,23 +10,23 @@ import { deleteAllEvents, deleteEvent, editEvent, + getRundown, reorderEvent, swapEvents, } from '../services/rundown-service/RundownService.js'; -import { get } from '../services/rundown-service/rundownCache.js'; -import { DataProvider } from '../classes/data-provider/DataProvider.js'; +import { get as getCachedRundown } from '../services/rundown-service/rundownCache.js'; // Create controller for GET request to '/events' // Returns - export const rundownGetAll: RequestHandler = async (_req, res) => { - const rundown = DataProvider.getRundown(); + const rundown = getRundown(); res.json(rundown); }; // Create controller for GET request to '/events/cached' // Returns - export const rundownGetCached: RequestHandler = async (_req: Request, res: Response) => { - const cachedRundown = get(); + const cachedRundown = getCachedRundown(); res.json(cachedRundown); }; diff --git a/apps/server/src/models/aliasDefinition.js b/apps/server/src/models/aliasDefinition.js deleted file mode 100644 index 18413c2be..000000000 --- a/apps/server/src/models/aliasDefinition.js +++ /dev/null @@ -1,5 +0,0 @@ -export const alias = { - enabled: false, - alias: '', - pathAndParams: '', -}; diff --git a/apps/server/src/models/eventsDefinition.ts b/apps/server/src/models/eventsDefinition.ts index 03721800a..f7c03f992 100644 --- a/apps/server/src/models/eventsDefinition.ts +++ b/apps/server/src/models/eventsDefinition.ts @@ -1,4 +1,12 @@ -import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types'; +import { + EndAction, + OntimeBlock, + OntimeDelay, + OntimeEvent, + SupportedEvent, + TimeStrategy, + TimerType, +} from 'ontime-types'; export const event: Omit = { title: '', @@ -7,6 +15,8 @@ export const event: Omit = { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockDuration, + linkStart: null, timeStart: 0, timeEnd: 0, duration: 0, diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index e0fb9bcd5..6f0f4f7b4 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -11,7 +11,6 @@ import { } from 'ontime-types'; import { getCueCandidate } from 'ontime-utils'; -import { DataProvider } from '../../classes/data-provider/DataProvider.js'; import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js'; import { sendRefetch } from '../../adapters/websocketAux.js'; import { logger } from '../../classes/Logger.js'; @@ -26,7 +25,7 @@ function generateEvent(eventData: Partial | Partial | const id = cache.getUniqueId(); if (isOntimeEvent(eventData)) { - return createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent; + return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as OntimeEvent; } if (isOntimeDelay(eventData)) { @@ -188,7 +187,7 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?: * @return {array} */ export function getRundown(): OntimeRundown { - return DataProvider.getRundown(); + return cache.getPersistedRundown(); } /** @@ -196,7 +195,7 @@ export function getRundown(): OntimeRundown { * @return {array} */ export function getTimedEvents(): OntimeEvent[] { - return DataProvider.getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[]; + return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[]; } /** @@ -204,7 +203,7 @@ export function getTimedEvents(): OntimeEvent[] { * @return {array} */ export function getPlayableEvents(): OntimeEvent[] { - return DataProvider.getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[]; + return getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[]; } /** @@ -288,7 +287,6 @@ export function findNext(currentEventId?: string): OntimeEvent | null { } export async function setRundown(rundown: OntimeRundown) { - await DataProvider.setRundown(rundown); cache.init(rundown); notifyChanges({ timer: true }); } 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 454bce278..9cfd4808a 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -1,7 +1,174 @@ -import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types'; +import { + EndAction, + OntimeBlock, + OntimeDelay, + OntimeEvent, + OntimeRundown, + SupportedEvent, + TimeStrategy, + TimerType, +} from 'ontime-types'; import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js'; -import { add, batchEdit, edit, remove, reorder, swap } from '../rundownCache.js'; +import { add, batchEdit, edit, generate, remove, reorder, swap } from '../rundownCache.js'; + +describe('init() function', () => { + it('creates normalised versions of a given rundown', () => { + const testRundown: OntimeRundown = [ + { type: SupportedEvent.Event, id: '1' } as OntimeEvent, + { type: SupportedEvent.Block, id: '2' } as OntimeBlock, + { type: SupportedEvent.Delay, id: '3' } as OntimeDelay, + ]; + + const initResult = generate(testRundown); + expect(initResult.order.length).toBe(3); + expect(initResult.order).toStrictEqual(['1', '2', '3']); + expect(initResult.rundown['1'].type).toBe(SupportedEvent.Event); + expect(initResult.rundown['2'].type).toBe(SupportedEvent.Block); + expect(initResult.rundown['3'].type).toBe(SupportedEvent.Delay); + }); + + it('calculates delays versions of a given rundown', () => { + const testRundown: OntimeRundown = [ + { type: SupportedEvent.Delay, id: '1', duration: 100 } as OntimeDelay, + { type: SupportedEvent.Event, id: '2', timeStart: 1 } as OntimeEvent, + { type: SupportedEvent.Block, id: '3' } as OntimeBlock, + { type: SupportedEvent.Event, id: '4', timeStart: 2 } as OntimeEvent, + ]; + + const initResult = generate(testRundown); + expect(initResult.order.length).toBe(4); + expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(100); + expect((initResult.rundown['4'] as OntimeEvent).delay).toBe(0); + }); + + it('links times across events', () => { + const testRundown: OntimeRundown = [ + { + type: SupportedEvent.Event, + id: '1', + timeStart: 1, + duration: 1, + timeEnd: 2, + timeStrategy: TimeStrategy.LockEnd, + } as OntimeEvent, + { + type: SupportedEvent.Event, + id: '2', + timeStart: 11, + duration: 1, + timeEnd: 12, + linkStart: '1', + timeStrategy: TimeStrategy.LockEnd, + } as OntimeEvent, + { type: SupportedEvent.Block, id: 'block' } as OntimeBlock, + { type: SupportedEvent.Delay, id: 'delay' } as OntimeDelay, + { + type: SupportedEvent.Event, + id: '3', + timeStart: 21, + duration: 1, + timeEnd: 22, + linkStart: '2', + timeStrategy: TimeStrategy.LockEnd, + } as OntimeEvent, + ]; + + const initResult = generate(testRundown); + expect(initResult.order.length).toBe(5); + expect((initResult.rundown['2'] as OntimeEvent).timeStart).toBe(2); + expect((initResult.rundown['2'] as OntimeEvent).timeEnd).toBe(12); + expect((initResult.rundown['2'] as OntimeEvent).duration).toBe(10); + + expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(12); + expect((initResult.rundown['3'] as OntimeEvent).timeEnd).toBe(22); + expect((initResult.rundown['3'] as OntimeEvent).duration).toBe(10); + + expect(initResult.links['1']).toBe('2'); + expect(initResult.links['2']).toBe('3'); + }); + + it('links times across events, reordered', () => { + const testRundown: OntimeRundown = [ + { type: SupportedEvent.Event, id: '1', timeStart: 1, timeEnd: 2 } as OntimeEvent, + { type: SupportedEvent.Event, id: '3', timeStart: 21, timeEnd: 22, linkStart: '2' } as OntimeEvent, + { type: SupportedEvent.Event, id: '2', timeStart: 11, timeEnd: 12, linkStart: '1' } as OntimeEvent, + ]; + + const initResult = generate(testRundown); + expect(initResult.order.length).toBe(3); + expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(2); + expect(initResult.links['1']).toBe('3'); + expect(initResult.links['3']).toBe('2'); + }); + + it('handles updating event sequence', () => { + const testRundown: OntimeRundown = [ + { + type: SupportedEvent.Event, + id: '97cc3e', + timeStart: 0, + timeEnd: 600000, + duration: 600000, + timeStrategy: TimeStrategy.LockDuration, + linkStart: null, + } as OntimeEvent, + { + type: SupportedEvent.Event, + id: 'e01948', + timeStart: 600000, + timeEnd: 601000, + duration: 85801000, // <------------- value out of sync + timeStrategy: TimeStrategy.LockEnd, + linkStart: '97cc3e', + } as OntimeEvent, + { + type: SupportedEvent.Event, + id: '25c1af', + timeStart: 100, // <------------- value out of sync + timeEnd: 602000, + duration: 0, + timeStrategy: TimeStrategy.LockEnd, + linkStart: 'e01948', + } as OntimeEvent, + ]; + + const initResult = generate(testRundown); + expect(initResult.rundown).toMatchObject({ + '97cc3e': { + timeStart: 0, + timeEnd: 600000, + duration: 600000, + timeStrategy: 'lock-duration', + linkStart: null, + }, + e01948: { + timeStart: 600000, + timeEnd: 601000, + duration: 1000, + timeStrategy: 'lock-end', + linkStart: '97cc3e', + }, + '25c1af': { + timeStart: 601000, + timeEnd: 602000, + duration: 1000, + timeStrategy: 'lock-end', + linkStart: 'e01948', + }, + }); + }); + + it('deletes links if invalid', () => { + const testRundown: OntimeRundown = [ + { type: SupportedEvent.Event, id: '1', timeStart: 1, linkStart: '10' } as OntimeEvent, + ]; + const initResult = generate(testRundown); + expect(initResult.order.length).toBe(1); + expect((initResult.rundown['1'] as OntimeEvent).timeStart).toBe(1); + expect(Object.keys(initResult.links).length).toBe(0); + }); +}); describe('add() mutation', () => { test('adds an event to the rundown', () => { @@ -137,6 +304,8 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -172,6 +341,8 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1200000, duration: 0, @@ -207,6 +378,8 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -242,6 +415,8 @@ describe('calculateRuntimeDelays', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1800000, duration: 600000, @@ -286,6 +461,8 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -322,6 +499,8 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1200000, duration: 0, @@ -358,6 +537,8 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -394,6 +575,8 @@ describe('getDelayAt()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1800000, duration: 600000, @@ -456,6 +639,8 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -492,6 +677,8 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1200000, duration: 0, @@ -528,6 +715,8 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 600000, timeEnd: 1200000, duration: 600000, @@ -564,6 +753,8 @@ describe('calculateRuntimeDelaysFrom()', () => { note: '', endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, timeStart: 1200000, timeEnd: 1800000, duration: 600000, diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 0a848adf6..535712659 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -6,80 +6,123 @@ import { OntimeRundown, OntimeRundownEntry, } from 'ontime-types'; -import { generateId, deleteAtIndex, insertAtIndex, reorderArray, swapEventData } from 'ontime-utils'; +import { + generateId, + deleteAtIndex, + insertAtIndex, + reorderArray, + swapEventData, + getLinkedTimes, + formatFromMillis, +} from 'ontime-utils'; import { DataProvider } from '../../classes/data-provider/DataProvider.js'; import { createPatch } from '../../utils/parser.js'; import { apply } from './delayUtils.js'; -type NormalisedRundown = Record; +type EventID = string; +type NormalisedRundown = Record; + +let persistedRundown: OntimeRundown = []; +/** Utility function gets rundown from DataProvider */ +export const getPersistedRundown = (): OntimeRundown => persistedRundown; let rundown: NormalisedRundown = {}; -let order: string[] = []; +let order: EventID[] = []; let revision = 0; let isStale = true; -/** - * Utility initialises cache - * @param persistedRundown - */ -export function init(persistedRundown: Readonly) { - // we decided to try and re-write this dataset for every change - // instead of maintaining logic to update it - rundown = {}; - order = []; +let links: Record = {}; - let accumulatedDelay = 0; - for (let i = 0; i < persistedRundown.length; i++) { - const event = persistedRundown[i]; - - // calculate delays - if (isOntimeDelay(event)) { - accumulatedDelay += event.duration; - } else if (isOntimeBlock(event)) { - accumulatedDelay = 0; - } else if (isOntimeEvent(event)) { - event.delay = accumulatedDelay; - } - - order.push(event.id); - rundown[event.id] = { ...event }; - } - isStale = false; +export async function init(initialRundown: OntimeRundown) { + persistedRundown = structuredClone(initialRundown); + generate(); + await DataProvider.setRundown(persistedRundown); } /** - * Returns an ID guaranteed to be unique - * @returns + * Utility initialises cache + * @param rundown */ -export function getUniqueId(persistedRundown: Readonly = getPersistedRundown()): string { +export function generate(initialRundown: OntimeRundown = persistedRundown) { + // we decided to re-write this dataset for every change + // instead of maintaining logic to update it + + function getLink(currentIndex: number): OntimeEvent | null { + // currently the link is the previous event + for (let i = currentIndex - 1; i >= 0; i--) { + const event = initialRundown[i]; + if (isOntimeEvent(event)) { + return event; + } + } + return null; + } + + rundown = {}; + order = []; + links = {}; + + let accumulatedDelay = 0; + for (let i = 0; i < initialRundown.length; i++) { + const currentEvent = initialRundown[i]; + let updatedEvent = { ...currentEvent }; + + // handle links + if (isOntimeEvent(updatedEvent)) { + if (updatedEvent.linkStart) { + const linkedEvent = getLink(i); + // link is always the previous event for now + if (linkedEvent) { + links[linkedEvent.id] = currentEvent.id; + + const timePatch = getLinkedTimes(updatedEvent, linkedEvent); + updatedEvent = { ...updatedEvent, ...timePatch }; + } else { + updatedEvent.linkStart = null; + } + // update the persisted event + initialRundown[i] = updatedEvent; + } + } + + // calculate delays + if (isOntimeDelay(updatedEvent)) { + accumulatedDelay += updatedEvent.duration; + } else if (isOntimeBlock(updatedEvent)) { + accumulatedDelay = 0; + } else if (isOntimeEvent(updatedEvent)) { + updatedEvent.delay = accumulatedDelay; + } + + order.push(updatedEvent.id); + rundown[updatedEvent.id] = { ...updatedEvent }; + } + + isStale = false; + return { rundown, order, links }; +} + +/** Returns an ID guaranteed to be unique */ +export function getUniqueId(): string { + if (isStale) { + generate(); + } let id = ''; do { id = generateId(); - } while (!isIdUnique(persistedRundown, id)); + } while (Object.hasOwn(rundown, id)); return id; } -export function isIdUnique(persistedRundown: Readonly, eventId: string) { - if (isStale) { - init(persistedRundown); - } - return !Object.hasOwn(rundown, eventId); -} - +/** Returns index of an event with a given id */ export function getIndexOf(eventId: string) { if (isStale) { - init(getPersistedRundown()); + generate(); } return order.indexOf(eventId); } -/** - * Utility function gets rundown from DataProvider - * @returns {OntimeRundown} - */ -export const getPersistedRundown = (): OntimeRundown => DataProvider.getRundown(); - type RundownCache = { rundown: NormalisedRundown; order: string[]; @@ -93,7 +136,7 @@ type RundownCache = { export function get(): Readonly { if (isStale) { console.time('rundownCache__init'); - init(getPersistedRundown()); + generate(); console.timeEnd('rundownCache__init'); } return { @@ -117,21 +160,25 @@ type MutatingFn = (params: MutationParams) => MutatingRetur */ export function mutateCache(mutation: MutatingFn) { async function scopedMutation(params: T) { - const persistedRundown = getPersistedRundown(); const { newEvent, newRundown } = mutation({ ...params, persistedRundown }); revision = revision + 1; isStale = true; + persistedRundown = newRundown; - DataProvider.setRundown(newRundown); - // schedule the update to the next tick - - process.nextTick(() => { + // schedule a non priority cache update + setImmediate(() => { console.time('rundownCache__init'); - init(newRundown); + generate(); console.timeEnd('rundownCache__init'); }); + // TODO: should we trottle this? + // defer writing to the database + setImmediate(() => { + DataProvider.setRundown(persistedRundown); + }); + // TODO: could we return a patch object? return { newEvent }; } @@ -186,8 +233,12 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required { timeStart: 31500000, timeEnd: 32400000, duration: 32400000 - 31500000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: false, skip: false, colour: '', @@ -63,6 +66,8 @@ describe('test json parser with valid def', () => { timeStart: 32400000, timeEnd: 36000000, duration: 36000000 - 32400000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: true, skip: true, colour: 'red', @@ -93,6 +98,8 @@ describe('test json parser with valid def', () => { timeStart: 32400000, timeEnd: 37200000, duration: 37200000 - 32400000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: false, skip: false, colour: '', @@ -144,6 +151,8 @@ describe('test json parser with valid def', () => { timeStart: 39600000, timeEnd: 45000000, duration: 37200000 - 32400000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: false, skip: false, colour: '', @@ -174,6 +183,8 @@ describe('test json parser with valid def', () => { timeStart: 46800000, timeEnd: 50400000, duration: 37200000 - 32400000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: true, skip: true, colour: '', @@ -566,10 +577,12 @@ describe('test event validator', () => { }; // @ts-expect-error -- we know this is wrong, testing imports outside domain const validated = createEvent(event); - expect(typeof validated.timeStart).toEqual('number'); + assertType(validated.timeStart); + assertType(validated.timeEnd); + assertType(validated.duration); expect(validated.timeStart).toEqual(0); - expect(typeof validated.timeEnd).toEqual('number'); expect(validated.timeEnd).toEqual(2); + expect(validated.duration).toEqual(2); }); it('handles bad objects', () => { diff --git a/apps/server/src/utils/__tests__/sheetUtils.test.ts b/apps/server/src/utils/__tests__/sheetUtils.test.ts index 43dc064b9..0c1d3d14c 100644 --- a/apps/server/src/utils/__tests__/sheetUtils.test.ts +++ b/apps/server/src/utils/__tests__/sheetUtils.test.ts @@ -1,4 +1,4 @@ -import { EndAction, OntimeRundownEntry, SupportedEvent, TimerType } from 'ontime-types'; +import { EndAction, OntimeRundownEntry, SupportedEvent, TimeStrategy, TimerType } from 'ontime-types'; import { millisToString } from 'ontime-utils'; import { getA1Notation, cellRequestFromEvent } from '../sheetUtils.js'; @@ -29,6 +29,8 @@ describe('cellRequestFromEvent()', () => { note: 'Blue button on the right', timeStart: 46800000, timeEnd: 57600000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, endAction: EndAction.None, timerType: TimerType.CountDown, duration: 10800000, @@ -97,6 +99,8 @@ describe('cellRequestFromEvent()', () => { endAction: EndAction.None, timerType: TimerType.CountDown, duration: 10800000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: false, skip: false, colour: 'red', @@ -163,6 +167,8 @@ describe('cellRequestFromEvent()', () => { endAction: EndAction.None, timerType: TimerType.CountDown, duration: 10800000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: true, skip: false, colour: 'red', @@ -228,6 +234,8 @@ describe('cellRequestFromEvent()', () => { timeEnd: 57600000, endAction: EndAction.None, timerType: TimerType.CountDown, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, duration: 10800000, isPublic: true, skip: false, @@ -272,6 +280,8 @@ describe('cellRequestFromEvent()', () => { endAction: EndAction.None, timerType: TimerType.CountDown, duration: 10800000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: true, skip: false, colour: 'red', @@ -315,6 +325,8 @@ describe('cellRequestFromEvent()', () => { endAction: EndAction.None, timerType: TimerType.CountDown, duration: 10800000, + timeStrategy: TimeStrategy.LockEnd, + linkStart: null, isPublic: true, skip: false, colour: 'red', diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 21eea817e..20ba3f86d 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -7,6 +7,8 @@ import { validateTimerType, type ExcelImportOptions, validateTimes, + isKnownTimerType, + validateLinkStart, } from 'ontime-utils'; import { DatabaseModel, @@ -16,6 +18,7 @@ import { UserFields, EndAction, TimerType, + TimeStrategy, } from 'ontime-types'; import fs from 'fs'; @@ -38,7 +41,6 @@ import { import { parseExcelDate } from './time.js'; import { configService } from '../services/ConfigService.js'; import { coerceBoolean } from './coerceType.js'; -import { isKnownTimerType } from '../../../../packages/utils/src/validate-events/validateEvent.js'; export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; export const JSON_MIME = 'application/json'; @@ -335,16 +337,36 @@ export const parseJson = async (jsonData: Partial): Promise): OntimeEvent { if (Object.keys(patchEvent).length === 0) { return originalEvent; } - const { timeStart, timeEnd, duration } = validateTimes( + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes( patchEvent?.timeStart ?? originalEvent.timeStart, patchEvent?.timeEnd ?? originalEvent.timeEnd, patchEvent?.duration ?? originalEvent.duration, + patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy), ); + const maybeLinkStart = patchEvent.linkStart !== undefined ? patchEvent.linkStart : originalEvent.linkStart; return { id: originalEvent.id, @@ -355,6 +377,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial; @@ -281,7 +281,7 @@ class Sheet { }); if (readResponse.status === 200) { const { rundownMetadata } = parseExcel(readResponse.data.values, options); - const rundown = DataProvider.getRundown(); + const rundown = getRundown(); const titleRow = Object.values(rundownMetadata)[0]['row']; const updateRundown = Array(); diff --git a/e2e/tests/features/203-delay-block.spec.ts b/e2e/tests/features/203-delay-block.spec.ts index 88ca5954b..23bdf1187 100644 --- a/e2e/tests/features/203-delay-block.spec.ts +++ b/e2e/tests/features/203-delay-block.spec.ts @@ -7,15 +7,15 @@ test('delay blocks add time to events', async ({ page }) => { await page.getByRole('button', { name: 'Rundown menu' }).click(); await page.getByRole('menuitem', { name: 'Delete all events' }).click(); await page.getByRole('button', { name: 'Rundown menu' }).click(); - await page.getByRole('menuitem', { name: 'Add event at start' }).click(); + await page.getByRole('button', { name: 'Create event' }).click(); // add data to new event await page.getByTestId('rundown').getByPlaceholder('Start').click(); await page.getByTestId('rundown').getByPlaceholder('Start').fill('10m'); await page.getByTestId('rundown').getByPlaceholder('Start').press('Enter'); - await page.getByTestId('rundown').getByPlaceholder('End').click(); - await page.getByTestId('rundown').getByPlaceholder('End').fill('20m'); - await page.getByTestId('rundown').getByPlaceholder('End').press('Enter'); + await page.getByTestId('rundown').getByPlaceholder('Duration').click(); + await page.getByTestId('rundown').getByPlaceholder('Duration').fill('20m'); + await page.getByTestId('rundown').getByPlaceholder('Duration').press('Enter'); // add delay block await page.getByRole('button', { name: 'Rundown menu' }).click(); @@ -25,11 +25,11 @@ test('delay blocks add time to events', async ({ page }) => { await page.getByTestId('delay-input').click(); await page.getByTestId('delay-input').fill('2m'); await page.getByTestId('delay-input').press('Enter'); - await page.getByText('New start: 00:12').click(); + await page.getByText('New start 00:12').click(); // make negative delay await page.getByText('Subtract time').click(); - await page.getByText('New start: 00:08').click(); + await page.getByText('New start 00:08').click(); // apply delay await page.getByRole('button', { name: 'Apply' }).click(); @@ -42,12 +42,12 @@ test('delay blocks add time to events', async ({ page }) => { await page.getByTestId('delay-input').click(); await page.getByTestId('delay-input').fill('10m'); await page.getByTestId('delay-input').press('Enter'); - await page.getByText('New start: 00:18').click(); + await page.getByText('New start 00:18').click(); // cancel delay await page.getByRole('button', { name: 'Cancel' }).click(); await expect(page.getByTestId('rundown').getByTestId('time-input-timeStart')).toHaveValue('00:08:00'); - await expect(page.getByText('New start: 00:18')).toHaveCount(0); + await expect(page.getByText('New start 00:18')).toHaveCount(0); }); test('delays are show correctly', async ({ page }) => { @@ -61,14 +61,14 @@ test('delays are show correctly', async ({ page }) => { await page.getByTestId('rundown').getByTestId('time-input-timeStart').click(); await page.getByTestId('rundown').getByTestId('time-input-timeStart').fill('10'); await page.getByTestId('rundown').getByTestId('time-input-timeStart').press('Enter'); - await page.getByTestId('rundown').getByTestId('time-input-timeEnd').click(); - await page.getByTestId('rundown').getByTestId('time-input-timeEnd').fill('20'); - await page.getByTestId('rundown').getByTestId('time-input-timeEnd').press('Enter'); + await page.getByTestId('rundown').getByTestId('time-input-duration').click(); + await page.getByTestId('rundown').getByTestId('time-input-duration').fill('10'); + await page.getByTestId('rundown').getByTestId('time-input-duration').press('Enter'); await page.getByText('Event title').click(); await page.getByPlaceholder('Event title').fill('test'); await page.getByPlaceholder('Event title').press('Enter'); - await page.getByText('SED').click({ button: 'right' }); + await page.getByTestId('entry-1').getByText('test').click({ button: 'right' }); await page.getByRole('menuitem', { name: 'Toggle public' }).click(); // add a delay @@ -79,11 +79,11 @@ test('delays are show correctly', async ({ page }) => { await page.getByTestId('delay-input').press('Enter'); // delay is shown in the editor - await page.getByText('New start: 00:11').click(); + await page.getByText('New start 00:11').click(); // delay is shown in the cuesheet await page.goto('http://localhost:4001/cuesheet'); - await page.getByRole('cell', { name: '+1 min' }).click(); + await page.getByRole('cell', { name: 'Delayed by 1 min' }).click(); // delay is NOT shown in the public view await page.goto('http://localhost:4001/public'); diff --git a/e2e/tests/features/204-editor-crud.spec.ts b/e2e/tests/features/204-editor-crud.spec.ts index da86c8099..aba543da8 100644 --- a/e2e/tests/features/204-editor-crud.spec.ts +++ b/e2e/tests/features/204-editor-crud.spec.ts @@ -17,16 +17,14 @@ test('CRUD operations on the rundown', async ({ page }) => { await page.getByTestId('quick-add-event').click(); // test quick add options - start is last end - await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').fill('20m'); + await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('20m'); await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); - await expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain( - '00:20:00', - ); + expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:20:00'); // test quick add options - event is public await page.locator('label').filter({ hasText: 'Event is public' }).click(); await page.getByTestId('quick-add-event').click(); - await expect(await page.getByTestId('entry-4').getByRole('img').nth(3)).toHaveAttribute('data-ispublic', 'true'); + await expect(page.getByTestId('entry-4').locator('#block-status')).toHaveAttribute('data-ispublic', 'true'); }); diff --git a/e2e/tests/features/205-operator.spec.ts b/e2e/tests/features/205-operator.spec.ts index 007e88d37..a0c1e01a9 100644 --- a/e2e/tests/features/205-operator.spec.ts +++ b/e2e/tests/features/205-operator.spec.ts @@ -10,18 +10,21 @@ test('smoke test operator', async ({ page }) => { await page.getByTestId('time-input-timeStart').fill('1m'); await page.getByTestId('time-input-timeStart').press('Enter'); - await page.getByTestId('time-input-durationOverride').fill('1m'); - await page.getByTestId('time-input-durationOverride').press('Enter'); + await page.getByTestId('time-input-duration').fill('1m'); + await page.getByTestId('time-input-duration').press('Enter'); await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); - await page.getByTestId('entry-2').getByTestId('time-input-durationOverride').fill('1m'); - await page.getByTestId('entry-2').getByTestId('time-input-durationOverride').press('Enter'); + await page.getByTestId('entry-2').getByTestId('lock__duration').click(); + await page.getByTestId('entry-2').getByTestId('time-input-duration').fill('1m'); + await page.getByTestId('entry-2').getByTestId('time-input-duration').press('Enter'); + await page.getByTestId('entry-2').getByTestId('time-input-duration').press('Enter'); await page.getByText('Start time is last end').click(); await page.getByTestId('quick-add-event').click(); - await page.getByTestId('entry-3').getByTestId('time-input-durationOverride').fill('1m'); - await page.getByTestId('entry-3').getByTestId('time-input-durationOverride').press('Enter'); + await page.getByTestId('entry-3').getByTestId('lock__duration').click(); + await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m'); + await page.getByTestId('entry-3').getByTestId('time-input-duration').press('Enter'); await page.getByRole('button', { name: 'Rundown menu' }).click(); await page.getByRole('menuitem', { name: 'Add block at start' }).click(); diff --git a/packages/types/src/definitions/TimeStrategy.type.ts b/packages/types/src/definitions/TimeStrategy.type.ts new file mode 100644 index 000000000..385c6bff9 --- /dev/null +++ b/packages/types/src/definitions/TimeStrategy.type.ts @@ -0,0 +1,4 @@ +export enum TimeStrategy { + LockEnd = 'lock-end', + LockDuration = 'lock-duration', +} diff --git a/packages/types/src/definitions/core/OntimeEvent.type.ts b/packages/types/src/definitions/core/OntimeEvent.type.ts index 9d15a9954..e525298e4 100644 --- a/packages/types/src/definitions/core/OntimeEvent.type.ts +++ b/packages/types/src/definitions/core/OntimeEvent.type.ts @@ -1,5 +1,7 @@ +import { MaybeString } from '../../index.js'; import { EndAction } from '../EndAction.type.js'; import { TimerType } from '../TimerType.type.js'; +import { TimeStrategy } from '../TimeStrategy.type.js'; export enum SupportedEvent { Event = 'event', @@ -32,6 +34,8 @@ export type OntimeEvent = OntimeBaseEvent & { note: string; endAction: EndAction; timerType: TimerType; + linkStart: MaybeString; // ID of event to link to + timeStrategy: TimeStrategy; timeStart: number; timeEnd: number; duration: number; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 085a5aa59..62aafe7d1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -11,6 +11,7 @@ export { SupportedEvent, } from './definitions/core/OntimeEvent.type.js'; export type { OntimeEntryCommonKeys, OntimeRundown, OntimeRundownEntry } from './definitions/core/Rundown.type.js'; +export { TimeStrategy } from './definitions/TimeStrategy.type.js'; export { TimerType } from './definitions/TimerType.type.js'; // ---> Project Data diff --git a/packages/utils/index.ts b/packages/utils/index.ts index ffc6d23c9..58752549d 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,7 +1,7 @@ // runtime utils export { validatePlayback } from './src/validate-action/validatePlayback.js'; -export { validateTimes } from './src/validate-events/validateEvent.js'; -export { calculateDuration } from './src/validate-events/validateEvent.js'; +export { isKnownTimerType, validateLinkStart, validateTimeStrategy } from './src/validate-events/validateEvent.js'; +export { calculateDuration, getLinkedTimes, validateTimes } from './src/validate-times/validateTimes.js'; // rundown utils export { sanitiseCue } from './src/cue-utils/cueUtils.js'; diff --git a/packages/utils/package.json b/packages/utils/package.json index c7b3c2fc2..2a992ce91 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -27,7 +27,7 @@ "ontime-types": "workspace:*", "prettier": "^3.0.3", "typescript": "^5.2.2", - "vitest": "^1.0.4" + "vitest": "^1.2.2" }, "sideEffects": false } diff --git a/packages/utils/src/validate-events/validateEvent.test.ts b/packages/utils/src/validate-events/validateEvent.test.ts index 76eca2c99..c7fa207fb 100644 --- a/packages/utils/src/validate-events/validateEvent.test.ts +++ b/packages/utils/src/validate-events/validateEvent.test.ts @@ -1,8 +1,7 @@ import { EndAction, TimerType } from 'ontime-types'; import { expect } from 'vitest'; -import { dayInMs } from '../timeConstants.js'; -import { calculateDuration, validateEndAction, validateTimerType, validateTimes } from './validateEvent.js'; +import { validateEndAction, validateTimerType } from './validateEvent.js'; describe('validateEndAction()', () => { it('recognises a string representation of an action', () => { @@ -29,90 +28,3 @@ describe('validateTimerType()', () => { expect(invalidType).toBe(TimerType.CountDown); }); }); - -describe('validateTimes()', () => { - it('passes through a well defined time list', () => { - const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5); - expect(timeStart).toBe(5); - expect(timeEnd).toBe(10); - expect(duration).toBe(5); - }); - - it('handles cases when no times are given', () => { - const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null); - expect(timeStart).toBe(0); - expect(timeEnd).toBe(0); - expect(duration).toBe(0); - }); - - it('calculates duration', () => { - const { timeStart, timeEnd, duration } = validateTimes(5, 10); - expect(timeStart).toBe(5); - expect(timeEnd).toBe(10); - expect(duration).toBe(5); - }); - - it('calculates end time', () => { - const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10); - expect(timeStart).toBe(5); - expect(timeEnd).toBe(15); - expect(duration).toBe(10); - }); - - it('handles events that finish the day after', () => { - const { timeStart, timeEnd, duration } = validateTimes(100, 10); - expect(timeStart).toBe(100); - expect(timeEnd).toBe(10); - expect(duration).toBe(dayInMs - 90); - }); - - it('corrects time in case of conflicts', () => { - const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15); - expect(timeStart).toBe(5); - expect(timeEnd).toBe(15); - expect(duration).toBe(10); - }); - - it('calculates start time', () => { - const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10); - expect(timeStart).toBe(5); - expect(timeEnd).toBe(15); - expect(duration).toBe(10); - }); - - it('calculates start and end time', () => { - const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10); - expect(timeStart).toBe(0); - expect(timeEnd).toBe(10); - expect(duration).toBe(10); - }); - - it('ensures values are integers', () => { - const { timeStart, timeEnd, duration } = validateTimes(0.000001, 10.312335342, 10); - expect(timeStart).toBe(0); - expect(timeEnd).toBe(10); - expect(duration).toBe(10); - }); -}); - -describe('calculateDuration()', () => { - describe('Given start and end values', () => { - it('is the difference between end and start', () => { - const duration = calculateDuration(10, 20); - expect(duration).toBe(10); - }); - }); - - describe('Handles edge cases', () => { - it('handles events that go over midnight', () => { - const duration = calculateDuration(51, 50); - expect(duration).toBe(dayInMs - 1); - }); - it('handles no difference', () => { - const duration1 = calculateDuration(0, 0); - const duration2 = calculateDuration(dayInMs, dayInMs); - expect(duration1).toBe(0); - expect(duration2).toBe(0); - }); - }); -}); diff --git a/packages/utils/src/validate-events/validateEvent.ts b/packages/utils/src/validate-events/validateEvent.ts index e34ec3acc..20200b493 100644 --- a/packages/utils/src/validate-events/validateEvent.ts +++ b/packages/utils/src/validate-events/validateEvent.ts @@ -1,6 +1,24 @@ -import { EndAction, TimerType } from 'ontime-types'; +import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types'; -import { dayInMs } from '../timeConstants.js'; +/** + * Check if a given value is a valid type of string, returns null otherwise + * @param {MaybeString} maybeLinkStart + * @returns {MaybeString} + */ +export function validateLinkStart(maybeLinkStart: unknown): MaybeString { + return typeof maybeLinkStart === 'string' ? maybeLinkStart : null; +} + +/** + * Check if a given value is a valid time strategy, returns the fallback otherwise + * @param {TimeStrategy} maybeTimeStrategy + * @returns {TimeStrategy} + */ +export function validateTimeStrategy(maybeTimeStrategy: unknown, fallback = TimeStrategy.LockDuration): TimeStrategy { + return Object.values(TimeStrategy).includes(maybeTimeStrategy as TimeStrategy) + ? (maybeTimeStrategy as TimeStrategy) + : fallback; +} /** * Checks if given value is a valid type of EndAction, returns the fallback otherwise @@ -23,71 +41,3 @@ export function validateTimerType(maybeTimerType: unknown, fallback = TimerType. export function isKnownTimerType(maybeTimerType: unknown) { return Object.values(TimerType).includes(maybeTimerType as TimerType); } - -/** - * @description calculates event duration considering midnight - * @param {number} timeStart - * @param {number} timeEnd - * @returns {number} - */ -export const calculateDuration = (timeStart: number, timeEnd: number): number => { - // Durations must be positive - if (timeEnd < timeStart) { - return timeEnd + dayInMs - timeStart; - } - return timeEnd - timeStart; -}; - -/** - * Converts a given value to an int, returns 0 otherwise - * @param value - * number - */ -function convertToInteger(value: unknown): number { - const result = Number(value); - return isNaN(result) ? 0 : Math.floor(result); -} - -/** - * Ensures the time input variables are valid in relationship to each other - * Infers values if necessary - * @param _start - * @param _end - * @param _duration - */ -export function validateTimes( - _start?: unknown, - _end?: unknown, - _duration?: unknown, -): { timeStart: number; duration: number; timeEnd: number } { - const timeStart = convertToInteger(_start); - const timeEnd = convertToInteger(_end); - const duration = convertToInteger(_duration); - - if (_start != null && _end != null) { - // Case 1. if we have start and end, duration must be derived - return { timeStart, duration: calculateDuration(timeStart, timeEnd), timeEnd }; - } - - if (_start == null && _end == null) { - if (_duration == null) { - // Case 2. no valid times were given - return { timeStart, duration, timeEnd }; - } - // Case 3. we have a duration and infer the rest - return { timeStart, duration, timeEnd: duration }; - } - - if (_start != null) { - // Case 5. with only start, we can calculate the rest - return { timeStart, duration, timeEnd: timeStart + duration }; - } - - if (_end != null) { - // Case 6. with only end, we can calculate the rest - return { timeStart: timeEnd - duration, duration, timeEnd }; - } - - // we should have covered all cases - return { timeStart, duration, timeEnd }; -} diff --git a/packages/utils/src/validate-times/validateTimes.test.ts b/packages/utils/src/validate-times/validateTimes.test.ts new file mode 100644 index 000000000..b2b1270b7 --- /dev/null +++ b/packages/utils/src/validate-times/validateTimes.test.ts @@ -0,0 +1,190 @@ +import { OntimeEvent, TimeStrategy } from 'ontime-types'; + +import { dayInMs } from '../timeConstants'; +import { calculateDuration, getLinkedTimes, validateTimes } from './validateTimes'; + +describe('validateTimes()', () => { + describe('when time strategy is inferred', () => { + it('passes through a well defined time list', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 10, 5); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(10); + expect(duration).toBe(5); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('handles cases when no times are given', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(null, undefined, null); + expect(timeStart).toBe(0); + expect(timeEnd).toBe(0); + expect(duration).toBe(0); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('calculates duration', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 10, undefined); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(10); + expect(duration).toBe(5); + expect(timeStrategy).toBe(TimeStrategy.LockEnd); + }); + + it('calculates end time', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, undefined, 10); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('handles events that finish the day after', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(100, 10, undefined); + expect(timeStart).toBe(100); + expect(timeEnd).toBe(10); + expect(duration).toBe(dayInMs - 90); + expect(timeStrategy).toBe(TimeStrategy.LockEnd); + }); + + it('corrects time in case of conflicts', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 15, 15); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('calculates start time', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(undefined, 15, 10); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('calculates start and end time', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(undefined, undefined, 10); + expect(timeStart).toBe(0); + expect(timeEnd).toBe(10); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + + it('ensures values are integers', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(0.000001, 10.312335342, 10); + expect(timeStart).toBe(0); + expect(timeEnd).toBe(10); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + it('prevents values from overflowing', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(dayInMs - 5, undefined, 10); + expect(timeStart).toBe(dayInMs - 5); + expect(timeEnd).toBe(5); + expect(duration).toBe(10); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + }); + describe('when time strategy is given', () => { + it('calculates end', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 20, 20, TimeStrategy.LockDuration); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(25); + expect(duration).toBe(20); + expect(timeStrategy).toBe(TimeStrategy.LockDuration); + }); + it('calculates duration', () => { + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 20, 20, TimeStrategy.LockEnd); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(20); + expect(duration).toBe(15); + expect(timeStrategy).toBe(TimeStrategy.LockEnd); + }); + }); +}); + +describe('calculateDuration()', () => { + describe('Given start and end values', () => { + it('is the difference between end and start', () => { + const duration = calculateDuration(10, 20); + expect(duration).toBe(10); + }); + }); + + describe('Handles edge cases', () => { + it('handles events that go over midnight', () => { + const duration = calculateDuration(51, 50); + expect(duration).toBe(dayInMs - 1); + }); + it('handles no difference', () => { + const duration1 = calculateDuration(0, 0); + const duration2 = calculateDuration(dayInMs, dayInMs); + expect(duration1).toBe(0); + expect(duration2).toBe(0); + }); + }); +}); + +describe('getLinkedTimes()', () => { + it('returns times with lock end', () => { + const source = { + timeStart: 5, + timeEnd: 15, + duration: 10, + } as OntimeEvent; + const target = { + timeStart: 10, + timeEnd: 20, + duration: 10, + timeStrategy: TimeStrategy.LockEnd, + } as OntimeEvent; + + const timePatch = getLinkedTimes(target, source); + expect(timePatch).toStrictEqual({ + timeStart: 15, + timeEnd: 20, + duration: 5, + }); + }); + + it('returns times with lock duration', () => { + const source = { + timeStart: 5, + timeEnd: 15, + duration: 10, + } as OntimeEvent; + const target = { + timeStart: 10, + timeEnd: 20, + duration: 10, + timeStrategy: TimeStrategy.LockDuration, + } as OntimeEvent; + + const timePatch = getLinkedTimes(target, source); + expect(timePatch).toStrictEqual({ + timeStart: 15, + timeEnd: 25, + duration: 10, + }); + }); + + it('prevents overflow', () => { + const source = { + timeStart: 5, + timeEnd: dayInMs - 5, + duration: 10, + } as OntimeEvent; + const target = { + timeStart: 0, + timeEnd: 20, + duration: 10, + timeStrategy: TimeStrategy.LockDuration, + } as OntimeEvent; + + const timePatch = getLinkedTimes(target, source); + expect(timePatch).toStrictEqual({ + timeStart: dayInMs - 5, + timeEnd: 5, + duration: 10, + }); + }); +}); diff --git a/packages/utils/src/validate-times/validateTimes.ts b/packages/utils/src/validate-times/validateTimes.ts new file mode 100644 index 000000000..f12356e9e --- /dev/null +++ b/packages/utils/src/validate-times/validateTimes.ts @@ -0,0 +1,129 @@ +import { OntimeEvent, TimeStrategy } from 'ontime-types'; + +import { dayInMs } from '../timeConstants.js'; +import { validateTimeStrategy } from '../validate-events/validateEvent.js'; + +export function getLinkedTimes( + target: OntimeEvent, + source: OntimeEvent, +): { timeStart: number; duration: number; timeEnd: number } { + const lockEnd = target.timeStrategy === TimeStrategy.LockEnd; + const lockDuration = target.timeStrategy === TimeStrategy.LockDuration; + const newStart = source.timeEnd; + + const timePatch = { + timeStart: newStart, + timeEnd: lockEnd ? target.timeEnd : calculateEnd(newStart, target.duration), + duration: lockDuration ? target.duration : calculateDuration(newStart, target.timeEnd), + }; + + return timePatch; +} + +function inferTimes( + _start?: unknown, + _end?: unknown, + _duration?: unknown, +): { timeStart: number; duration: number; timeEnd: number; timeStrategy: TimeStrategy } { + const timeStart = convertToInteger(_start); + const timeEnd = convertToInteger(_end); + const duration = convertToInteger(_duration); + + // TODO: prevent overflow + + if (_start != null && _end != null) { + // Case 1. if we have start and end, duration must be derived + return { + timeStart, + duration: calculateDuration(timeStart, timeEnd), + timeEnd, + timeStrategy: _duration != null ? TimeStrategy.LockDuration : TimeStrategy.LockEnd, + }; + } + + if (_start == null && _end == null) { + if (_duration == null) { + // Case 2. no valid times were given + return { timeStart: 0, duration: 0, timeEnd: 0, timeStrategy: TimeStrategy.LockDuration }; + } + // Case 3. we have a duration and infer the rest + return { timeStart, duration, timeEnd: duration, timeStrategy: TimeStrategy.LockDuration }; + } + + if (_start != null) { + // Case 5. with only start, we can calculate the rest + return { timeStart, duration, timeEnd: (timeStart + duration) % dayInMs, timeStrategy: TimeStrategy.LockDuration }; + } + + if (_end != null) { + // Case 6. with only end, we can calculate the rest + return { + timeStart: timeEnd - duration, + duration, + timeEnd, + timeStrategy: _duration != null ? TimeStrategy.LockDuration : TimeStrategy.LockEnd, + }; + } + + // we should have covered all cases + return { timeStart, duration, timeEnd, timeStrategy: TimeStrategy.LockDuration }; +} + +/** + * Ensures the time input variables are valid in relationship to each other + * Infers values if necessary + * @param _start + * @param _end + * @param _duration + */ +export function validateTimes( + _start?: unknown, + _end?: unknown, + _duration?: unknown, + _strategy?: TimeStrategy, +): { timeStart: number; duration: number; timeEnd: number; timeStrategy: TimeStrategy } { + if (_strategy == null) { + // if no strategy is given we infer it from given parameters + return inferTimes(_start, _end, _duration); + } + + const timeStrategy = validateTimeStrategy(_strategy); + const timeStart = convertToInteger(_start); + let timeEnd = convertToInteger(_end); + let duration = convertToInteger(_duration); + + if (timeStrategy === TimeStrategy.LockEnd) { + duration = calculateDuration(timeStart, timeEnd); + } else { + timeEnd = calculateEnd(timeStart, duration); + } + return { timeStart, duration, timeEnd, timeStrategy }; +} + +/** + * @description calculates event duration considering midnight + * @param {number} timeStart + * @param {number} timeEnd + * @returns {number} + */ +export function calculateDuration(timeStart: number, timeEnd: number): number { + // Durations must be positive + if (timeEnd < timeStart) { + return timeEnd + dayInMs - timeStart; + } + return timeEnd - timeStart; +} + +export function calculateEnd(timeStart: number, duration: number): number { + return (timeStart + duration) % dayInMs; +} + +/** + * Converts a given value to an int, returns 0 otherwise + * @param value + * number + */ +function convertToInteger(value: unknown): number { + const result = Number(value); + return isNaN(result) ? 0 : Math.floor(result); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6b44cc41..b9cdc5e26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,8 +130,8 @@ importers: version: 4.4.7(@types/react@18.0.26)(react@18.2.0) devDependencies: '@sentry/vite-plugin': - specifier: ^2.10.2 - version: 2.10.2 + specifier: ^2.14.0 + version: 2.14.0 '@tanstack/eslint-plugin-query': specifier: ^5.8.4 version: 5.8.4(eslint@8.53.0)(typescript@5.2.2) @@ -164,7 +164,7 @@ importers: version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.2.1(vite@5.0.10) + version: 4.2.1(vite@5.1.0) eslint: specifier: ^8.53.0 version: 8.53.0 @@ -208,20 +208,20 @@ importers: specifier: ^5.2.2 version: 5.2.2 vite: - specifier: ^5.0.10 - version: 5.0.10(@types/node@18.11.18)(sass@1.57.1) + specifier: ^5.1.0 + version: 5.1.0(@types/node@18.11.18)(sass@1.57.1) vite-plugin-compression2: - specifier: ^0.11.0 - version: 0.11.0 + specifier: ^0.12.0 + version: 0.12.0 vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(typescript@5.2.2)(vite@5.0.10) + version: 4.2.0(typescript@5.2.2)(vite@5.1.0) vite-tsconfig-paths: - specifier: ^4.2.2 - version: 4.2.2(typescript@5.2.2)(vite@5.0.10) + specifier: ^4.3.1 + version: 4.3.1(typescript@5.2.2)(vite@5.1.0) vitest: - specifier: ^1.0.4 - version: 1.0.4(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) + specifier: ^1.2.2 + version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) apps/electron: devDependencies: @@ -359,8 +359,8 @@ importers: specifier: ^5.2.2 version: 5.2.2 vitest: - specifier: ^1.0.4 - version: 1.0.4(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) + specifier: ^1.2.2 + version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) packages/types: devDependencies: @@ -420,8 +420,8 @@ importers: specifier: ^5.2.2 version: 5.2.2 vitest: - specifier: ^1.0.4 - version: 1.0.4(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) + specifier: ^1.2.2 + version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) packages: @@ -465,6 +465,29 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/core@7.18.5: + resolution: {integrity: sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.5) + '@babel/helpers': 7.23.6 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/core@7.23.6: resolution: {integrity: sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==} engines: {node: '>=6.9.0'} @@ -543,6 +566,20 @@ packages: '@babel/types': 7.23.6 dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.18.5): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.6): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} @@ -2593,6 +2630,11 @@ packages: '@sentry/utils': 7.92.0 dev: false + /@sentry/babel-plugin-component-annotate@2.14.0: + resolution: {integrity: sha512-FWU4+Lx6fgxjAkwmc3S9j1Q/6pqKZyZzfi52B+8WMNw7a5QjGXgxc5ucBazZYgrcsJKCFBp4QG3PPxNAieFimQ==} + engines: {node: '>= 14'} + dev: true + /@sentry/browser@7.92.0: resolution: {integrity: sha512-loMr02/zQ38u8aQhYLtIBg0i5n3ps2e3GUXrt3CdsJQdkRYfa62gcrE7SzvoEpMVHTk7VOI4fWGht8cWw/1k3A==} engines: {node: '>=8'} @@ -2605,13 +2647,15 @@ packages: '@sentry/utils': 7.92.0 dev: false - /@sentry/bundler-plugin-core@2.10.2: - resolution: {integrity: sha512-7IoekLtROlJZqTxtHQ3IhocBuf9dsEq+JjqlHMyZXoq+QKuvJFvMd/4T+r6KjZ15kMZOIkR+spK3V7duH201hw==} + /@sentry/bundler-plugin-core@2.14.0: + resolution: {integrity: sha512-jVM47EPs8Na2z5HOWgthLFhpHLU9hwL2wY4TzHEnS1Bj+ODgXFa8QcIxQR2SO+W+L8YhSbY7z+BpPsYTpeZWUg==} engines: {node: '>= 14'} dependencies: + '@babel/core': 7.18.5 + '@sentry/babel-plugin-component-annotate': 2.14.0 '@sentry/cli': 2.23.0 '@sentry/node': 7.88.0 - '@sentry/utils': 7.88.0 + '@sentry/utils': 7.92.0 dotenv: 16.3.1 find-up: 5.0.0 glob: 9.3.2 @@ -2767,7 +2811,6 @@ packages: /@sentry/types@7.92.0: resolution: {integrity: sha512-APmSOuZuoRGpbPpPeYIbMSplPjiWNLZRQa73QiXuTflW4Tu/ItDlU8hOa2+A6JKVkJCuD2EN6yUrxDGSMyNXeg==} engines: {node: '>=8'} - dev: false /@sentry/utils@7.88.0: resolution: {integrity: sha512-ukminfRmdBXTzk49orwJf3Lu3hR60ZRHjE2a4IXwYhyDT6JJgJqgsq1hzGXx0AyFfyS4WhfZ6QUBy7fu3BScZQ==} @@ -2781,13 +2824,12 @@ packages: engines: {node: '>=8'} dependencies: '@sentry/types': 7.92.0 - dev: false - /@sentry/vite-plugin@2.10.2: - resolution: {integrity: sha512-30uu0L8ZCpAKOxAXmtyqwL06sG8UEBXGY5mxUDITyQYDf8pKuiOEf5018KlEDjhYVypfMQH3jq5xXUUka+/ipg==} + /@sentry/vite-plugin@2.14.0: + resolution: {integrity: sha512-Y25rBys8hDkbcqRBBGc5kez8JoQ0K/IfQzVHzuVFgtavcwQVhTUKbP4P7tPXI6P9gf4sRkycPnMyJzgvf5XAIQ==} engines: {node: '>= 14'} dependencies: - '@sentry/bundler-plugin-core': 2.10.2 + '@sentry/bundler-plugin-core': 2.14.0 unplugin: 1.0.1 transitivePeerDependencies: - encoding @@ -3648,7 +3690,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react@4.2.1(vite@5.0.10): + /@vitejs/plugin-react@4.2.1(vite@5.1.0): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3659,45 +3701,46 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.0.10(@types/node@18.11.18)(sass@1.57.1) + vite: 5.1.0(@types/node@18.11.18)(sass@1.57.1) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.0.4: - resolution: {integrity: sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ==} + /@vitest/expect@1.2.2: + resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} dependencies: - '@vitest/spy': 1.0.4 - '@vitest/utils': 1.0.4 + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 chai: 4.3.10 dev: true - /@vitest/runner@1.0.4: - resolution: {integrity: sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ==} + /@vitest/runner@1.2.2: + resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==} dependencies: - '@vitest/utils': 1.0.4 + '@vitest/utils': 1.2.2 p-limit: 5.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@1.0.4: - resolution: {integrity: sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA==} + /@vitest/snapshot@1.2.2: + resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} dependencies: magic-string: 0.30.5 pathe: 1.1.1 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.0.4: - resolution: {integrity: sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA==} + /@vitest/spy@1.2.2: + resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==} dependencies: tinyspy: 2.2.0 dev: true - /@vitest/utils@1.0.4: - resolution: {integrity: sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA==} + /@vitest/utils@1.2.2: + resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==} dependencies: diff-sequences: 29.6.3 + estree-walker: 3.0.3 loupe: 2.3.7 pretty-format: 29.7.0 dev: true @@ -3757,6 +3800,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.11.2: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} @@ -3902,6 +3950,31 @@ packages: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} dev: false + /archiver-utils@4.0.1: + resolution: {integrity: sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==} + engines: {node: '>= 12.0.0'} + dependencies: + glob: 8.1.0 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: true + + /archiver@6.0.1: + resolution: {integrity: sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==} + engines: {node: '>= 12.0.0'} + dependencies: + archiver-utils: 4.0.1 + async: 3.2.5 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 5.0.1 + dev: true + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -4017,6 +4090,10 @@ packages: - debug dev: false + /b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + dev: true + /babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -4446,6 +4523,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /compress-commons@5.0.1: + resolution: {integrity: sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==} + engines: {node: '>= 12.0.0'} + dependencies: + crc-32: 1.2.2 + crc32-stream: 5.0.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: true + /compute-scroll-into-view@1.0.20: resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} dev: false @@ -4485,7 +4572,6 @@ packages: /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: false /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4519,7 +4605,6 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} @@ -4556,6 +4641,20 @@ packages: typescript: 5.2.2 dev: true + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: true + + /crc32-stream@5.0.0: + resolution: {integrity: sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==} + engines: {node: '>= 12.0.0'} + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + dev: true + /crc@3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} requiresBuild: true @@ -5409,6 +5508,12 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5583,6 +5688,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: true + /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -5923,6 +6032,17 @@ packages: path-is-absolute: 1.0.1 dev: true + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + /glob@9.3.2: resolution: {integrity: sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA==} engines: {node: '>=16 || 14 >=14.17'} @@ -6535,7 +6655,6 @@ packages: /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -6573,7 +6692,7 @@ packages: chalk: 4.1.2 diff-sequences: 29.6.3 jest-get-type: 29.2.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 dev: true /jest-get-type@29.2.0: @@ -6588,7 +6707,7 @@ packages: chalk: 4.1.2 jest-diff: 29.3.1 jest-get-type: 29.2.0 - pretty-format: 29.3.1 + pretty-format: 29.7.0 dev: true /jest-message-util@29.3.1: @@ -6601,7 +6720,7 @@ packages: chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 - pretty-format: 29.3.1 + pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 dev: true @@ -6757,6 +6876,13 @@ packages: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} dev: true + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.7 + dev: true + /levn@0.3.0: resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} engines: {node: '>= 0.8.0'} @@ -7557,8 +7683,8 @@ packages: xmlbuilder: 15.1.1 dev: true - /postcss@8.4.32: - resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} + /postcss@8.4.35: + resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.7 @@ -7618,7 +7744,6 @@ packages: /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} @@ -7690,6 +7815,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: true + /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -7911,7 +8040,21 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + dependencies: + minimatch: 5.1.6 + dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -8076,7 +8219,6 @@ packages: /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: false /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -8390,6 +8532,13 @@ packages: engines: {node: '>=10.0.0'} dev: false + /streamx@2.15.7: + resolution: {integrity: sha512-NPEKS5+yjyo597eafGbKW5ujh7Sm6lDLHZQd/lRSz6S0VarpADBJItqfB4PnwpS+472oob1GX5cCY9vzfJpHUA==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: true + /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -8446,7 +8595,6 @@ packages: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: safe-buffer: 5.1.2 - dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -8536,6 +8684,14 @@ packages: tslib: 2.6.2 dev: true + /tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.15.7 + dev: true + /tar@6.2.0: resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} engines: {node: '>=10'} @@ -8567,8 +8723,8 @@ packages: resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} dev: true - /tinypool@0.8.1: - resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==} + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} engines: {node: '>=14.0.0'} dev: true @@ -8699,12 +8855,12 @@ packages: yn: 3.1.1 dev: true - /tsconfck@2.1.2(typescript@5.2.2): - resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} - engines: {node: ^14.13.1 || ^16 || >=18} + /tsconfck@3.0.2(typescript@5.2.2): + resolution: {integrity: sha512-6lWtFjwuhS3XI4HsX4Zg0izOI3FU/AI9EGVlPEUMDIhvLPMD4wkiof0WCoDgW7qY+Dy198g4d9miAqUHWHFH6Q==} + engines: {node: ^18 || >=20} hasBin: true peerDependencies: - typescript: ^4.3.5 || ^5.0.0 + typescript: ^5.0.0 peerDependenciesMeta: typescript: optional: true @@ -9003,7 +9159,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} @@ -9040,8 +9195,8 @@ packages: dev: true optional: true - /vite-node@1.0.4(@types/node@18.11.18)(sass@1.57.1): - resolution: {integrity: sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==} + /vite-node@1.2.2(@types/node@18.11.18)(sass@1.57.1): + resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -9049,7 +9204,7 @@ packages: debug: 4.3.4 pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.0.10(@types/node@18.11.18)(sass@1.57.1) + vite: 5.1.0(@types/node@18.11.18)(sass@1.57.1) transitivePeerDependencies: - '@types/node' - less @@ -9061,15 +9216,16 @@ packages: - terser dev: true - /vite-plugin-compression2@0.11.0: - resolution: {integrity: sha512-U6oEyRXZD26BynOgD/tStNTbQOLPt96aQNj/gdJTicKVYCQCdlV7QdmSF7VEhSyjiS59pQRhiMBu/uajprxWLA==} + /vite-plugin-compression2@0.12.0: + resolution: {integrity: sha512-9zdEF9xKVezETSF1l1bHoOk8LNoKIHB+DZVgSIGuGWaYupwFmsAGh0uwRcmK6rVHacxQRBECVYdtfc65DPDRfg==} dependencies: '@rollup/pluginutils': 5.1.0 + archiver: 6.0.1 transitivePeerDependencies: - rollup dev: true - /vite-plugin-svgr@4.2.0(typescript@5.2.2)(vite@5.0.10): + /vite-plugin-svgr@4.2.0(typescript@5.2.2)(vite@5.1.0): resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} peerDependencies: vite: ^2.6.0 || 3 || 4 || 5 @@ -9077,15 +9233,15 @@ packages: '@rollup/pluginutils': 5.1.0 '@svgr/core': 8.1.0(typescript@5.2.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - vite: 5.0.10(@types/node@18.11.18)(sass@1.57.1) + vite: 5.1.0(@types/node@18.11.18)(sass@1.57.1) transitivePeerDependencies: - rollup - supports-color - typescript dev: true - /vite-tsconfig-paths@4.2.2(typescript@5.2.2)(vite@5.0.10): - resolution: {integrity: sha512-dq0FjyxHHDnp0uS3P12WEOX2W7NeuLzX9AWP38D7Zw2CTbFErapwQVlCiT5DMJcVWKQ1MMdTe92PZl/rBQ7qcw==} + /vite-tsconfig-paths@4.3.1(typescript@5.2.2)(vite@5.1.0): + resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} peerDependencies: vite: '*' peerDependenciesMeta: @@ -9094,15 +9250,15 @@ packages: dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 2.1.2(typescript@5.2.2) - vite: 5.0.10(@types/node@18.11.18)(sass@1.57.1) + tsconfck: 3.0.2(typescript@5.2.2) + vite: 5.1.0(@types/node@18.11.18)(sass@1.57.1) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.0.10(@types/node@18.11.18)(sass@1.57.1): - resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==} + /vite@5.1.0(@types/node@18.11.18)(sass@1.57.1): + resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9131,15 +9287,15 @@ packages: dependencies: '@types/node': 18.11.18 esbuild: 0.19.10 - postcss: 8.4.32 + postcss: 8.4.35 rollup: 4.9.1 sass: 1.57.1 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@1.0.4(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1): - resolution: {integrity: sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==} + /vitest@1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1): + resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9164,12 +9320,12 @@ packages: optional: true dependencies: '@types/node': 18.11.18 - '@vitest/expect': 1.0.4 - '@vitest/runner': 1.0.4 - '@vitest/snapshot': 1.0.4 - '@vitest/spy': 1.0.4 - '@vitest/utils': 1.0.4 - acorn-walk: 8.3.1 + '@vitest/expect': 1.2.2 + '@vitest/runner': 1.2.2 + '@vitest/snapshot': 1.2.2 + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + acorn-walk: 8.3.2 cac: 6.7.14 chai: 4.3.10 debug: 4.3.4 @@ -9182,9 +9338,9 @@ packages: std-env: 3.6.0 strip-literal: 1.3.0 tinybench: 2.5.1 - tinypool: 0.8.1 - vite: 5.0.10(@types/node@18.11.18)(sass@1.57.1) - vite-node: 1.0.4(@types/node@18.11.18)(sass@1.57.1) + tinypool: 0.8.2 + vite: 5.1.0(@types/node@18.11.18)(sass@1.57.1) + vite-node: 1.2.2(@types/node@18.11.18)(sass@1.57.1) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -9434,6 +9590,15 @@ packages: engines: {node: '>=12.20'} dev: true + /zip-stream@5.0.1: + resolution: {integrity: sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==} + engines: {node: '>= 12.0.0'} + dependencies: + archiver-utils: 4.0.1 + compress-commons: 5.0.1 + readable-stream: 3.6.2 + dev: true + /zustand@4.4.7(@types/react@18.0.26)(react@18.2.0): resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} engines: {node: '>=12.7.0'}