mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 00:13:53 +00:00
refactor: UI for linking events (#763)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||
if (typeof delayValue === 'number') {
|
||||
if (delayValue < 0) {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||
<span className={style.delaySymbol}>
|
||||
<IoChevronDown />
|
||||
</span>
|
||||
@@ -27,7 +27,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||
|
||||
if (delayValue > 0) {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
|
||||
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||
<span className={style.delaySymbol}>
|
||||
<IoChevronUp />
|
||||
</span>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T extends string> {
|
||||
@@ -11,11 +12,12 @@ interface TimeInputProps<T extends string> {
|
||||
submitHandler: (field: T, value: string) => void;
|
||||
time?: number;
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||
const { name, submitHandler, time = 0, placeholder, className } = props;
|
||||
const { name, submitHandler, time = 0, placeholder, disabled, className } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState<string>('');
|
||||
@@ -118,14 +120,16 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||
resetValue();
|
||||
}, [resetValue, time]);
|
||||
|
||||
const timeInputClass = className ? className : style.timeInput;
|
||||
const timeInputClasses = cx([style.timeInput, className]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
ref={inputRef}
|
||||
data-testid={`time-input-${name}`}
|
||||
className={timeInputClass}
|
||||
className={timeInputClasses}
|
||||
fontSize='1rem'
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='ontime-filled'
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
$input-font-size: 1rem;
|
||||
$input-delayed-border-color: $ontime-delay-text;
|
||||
|
||||
.timeInput {
|
||||
border: 1px solid transparent;
|
||||
&.delayed {
|
||||
border: 1px solid $input-delayed-border-color;
|
||||
}
|
||||
|
||||
.inputLeft,
|
||||
.inputButton {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
font-size: $input-font-size;
|
||||
letter-spacing: 1px;
|
||||
width: 7.5em;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
.inputButton {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.inputField {
|
||||
max-width: 7.75em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { InputGroup, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
@@ -12,30 +13,28 @@ interface TimeInputWithButtonProps<T extends string> {
|
||||
submitHandler: (field: T, value: string) => void;
|
||||
time?: number;
|
||||
hasDelay?: boolean;
|
||||
disabled?: boolean;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export default function TimeInputWithButton<T extends string>(props: TimeInputWithButtonProps<T>) {
|
||||
const { name, submitHandler, time, hasDelay, placeholder } = props;
|
||||
export default function TimeInputWithButton<T extends string>(props: PropsWithChildren<TimeInputWithButtonProps<T>>) {
|
||||
const { name, submitHandler, time, hasDelay, placeholder, disabled, children } = props;
|
||||
|
||||
const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]);
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' className={inputClasses} width='fit-content'>
|
||||
<InputLeftElement className={style.inputLeft}>
|
||||
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button size='sm' variant='ontime-subtle-white' className={style.inputButton} tabIndex={-1}>
|
||||
{placeholder.charAt(0)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<TimeInput<T>
|
||||
name={name}
|
||||
submitHandler={submitHandler}
|
||||
time={time}
|
||||
placeholder={placeholder}
|
||||
className={style.inputField}
|
||||
/>
|
||||
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<TimeInput<T>
|
||||
name={name}
|
||||
submitHandler={submitHandler}
|
||||
time={time}
|
||||
placeholder={placeholder}
|
||||
className={style.inputField}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
{children}
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<OntimeEvent> = { id: eventId };
|
||||
|
||||
if (!linkStart) {
|
||||
newEvent.linkStart = null;
|
||||
} else {
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 : (
|
||||
<>
|
||||
<EventBlockTimers eventId={eventId} timeStart={timeStart} timeEnd={timeEnd} duration={duration} delay={delay} />
|
||||
<div className={style.eventTimers}>
|
||||
<TimeInputFlow
|
||||
eventId={eventId}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
delay={delay}
|
||||
timeStrategy={timeStrategy}
|
||||
linkStart={linkStart}
|
||||
/>
|
||||
</div>
|
||||
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||
{next && (
|
||||
<Tooltip label='Next event' {...tooltipProps}>
|
||||
@@ -99,7 +113,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
selected={selected}
|
||||
disablePlayback={skip || isRolling}
|
||||
/>
|
||||
<div className={style.statusElements}>
|
||||
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
||||
<span className={style.eventNote}>{note}</span>
|
||||
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||
{selected && <EventBlockProgressBar playback={playback} />}
|
||||
@@ -117,10 +131,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
</Tooltip>
|
||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
|
||||
<span>
|
||||
<IoPeople
|
||||
className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`}
|
||||
data-ispublic={isPublic}
|
||||
/>
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<OntimeEvent> = { id: eventId, timeEnd: timeStart + timeInMillis };
|
||||
updateEvent(newEventData);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const overMidnight = timeStart > timeEnd;
|
||||
const hasDelay = delay !== 0;
|
||||
|
||||
return (
|
||||
<div className={style.eventTimers}>
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeStart}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='Start'
|
||||
/>
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeEnd}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='End'
|
||||
/>
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
time={duration}
|
||||
placeholder='Duration'
|
||||
/>
|
||||
{overMidnight && (
|
||||
<div className={style.timerNote}>
|
||||
<Tooltip
|
||||
label='End timer before start'
|
||||
openDelay={tooltipDelayFast}
|
||||
variant='ontime-ondark'
|
||||
shouldWrapChildren
|
||||
>
|
||||
<IoAlertCircleOutline />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventBlockTimers);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<OntimeEvent> = { 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 (
|
||||
<div className={style.column}>
|
||||
<div className={style.inline}>
|
||||
<div>
|
||||
<label className={inputTimeLabels} htmlFor='timeStart'>
|
||||
{startLabel}
|
||||
</label>
|
||||
<TimeInputWithButton
|
||||
name='timeStart'
|
||||
submitHandler={handleTimeSubmit}
|
||||
time={timeStart}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='Start'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={inputTimeLabels} htmlFor='timeEnd'>
|
||||
{endLabel}
|
||||
</label>
|
||||
<TimeInputWithButton
|
||||
name='timeEnd'
|
||||
submitHandler={handleTimeSubmit}
|
||||
time={timeEnd}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='End'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={style.inputLabel} htmlFor='durationOverride'>
|
||||
Duration
|
||||
</label>
|
||||
<TimeInputWithButton
|
||||
name='durationOverride'
|
||||
submitHandler={handleTimeSubmit}
|
||||
time={duration}
|
||||
placeholder='Duration'
|
||||
<>
|
||||
<div>
|
||||
<div className={style.inputLabel}>Event schedule</div>
|
||||
<div className={style.inline}>
|
||||
<TimeInputFlow
|
||||
eventId={eventId}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
timeStrategy={timeStrategy}
|
||||
linkStart={linkStart}
|
||||
delay={delay}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.delayLabel}>{delayLabel}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.splitTwo}>
|
||||
@@ -159,11 +135,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
<div>
|
||||
<span className={style.inputLabel}>Event Visibility</span>
|
||||
<label className={style.switchLabel}>
|
||||
<Switch isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
||||
<Switch size='sm' isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
||||
{isPublic ? 'Public' : 'Private'}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<OntimeEvent> = { 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 (
|
||||
<>
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeStart}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='Start'
|
||||
disabled={Boolean(linkStart)}
|
||||
>
|
||||
<InputRightElement className={activeStart} onClick={() => handleLink(!linkStart)}>
|
||||
<span className={style.timeLabel}>S</span>
|
||||
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
|
||||
</InputRightElement>
|
||||
</TimeInputWithButton>
|
||||
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeEnd}
|
||||
hasDelay={hasDelay}
|
||||
disabled={isLockedDuration}
|
||||
placeholder='End'
|
||||
>
|
||||
<InputRightElement
|
||||
className={activeEnd}
|
||||
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
|
||||
data-testid='lock__end'
|
||||
>
|
||||
<span className={style.timeLabel}>E</span>
|
||||
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||
</InputRightElement>
|
||||
</TimeInputWithButton>
|
||||
|
||||
<TimeInputWithButton<TimeActions>
|
||||
name='duration'
|
||||
submitHandler={handleSubmit}
|
||||
time={duration}
|
||||
disabled={isLockedEnd}
|
||||
placeholder='Duration'
|
||||
>
|
||||
<InputRightElement
|
||||
className={activeDuration}
|
||||
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
|
||||
data-testid='lock__duration'
|
||||
>
|
||||
<span className={style.timeLabel}>D</span>
|
||||
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||
</InputRightElement>
|
||||
</TimeInputWithButton>
|
||||
|
||||
{overMidnight && (
|
||||
<div className={style.timerNote}>
|
||||
<Tooltip
|
||||
label='Over midnight: end time is before start'
|
||||
openDelay={tooltipDelayFast}
|
||||
variant='ontime-ondark'
|
||||
shouldWrapChildren
|
||||
>
|
||||
<IoAlertCircleOutline />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TimeInputFlow);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+12
-6
@@ -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}`);
|
||||
|
||||
@@ -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<RundownCached>) => {
|
||||
const cachedRundown = get();
|
||||
const cachedRundown = getCachedRundown();
|
||||
res.json(cachedRundown);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export const alias = {
|
||||
enabled: false,
|
||||
alias: '',
|
||||
pathAndParams: '',
|
||||
};
|
||||
@@ -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<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
title: '',
|
||||
@@ -7,6 +15,8 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
linkStart: null,
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
duration: 0,
|
||||
|
||||
@@ -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<OntimeEvent> | Partial<OntimeDelay> |
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, OntimeRundownEntry>;
|
||||
type EventID = string;
|
||||
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
||||
|
||||
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<OntimeRundown>) {
|
||||
// we decided to try and re-write this dataset for every change
|
||||
// instead of maintaining logic to update it
|
||||
rundown = {};
|
||||
order = [];
|
||||
let links: Record<EventID, EventID> = {};
|
||||
|
||||
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<OntimeRundown> = 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<OntimeRundown>, 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<RundownCache> {
|
||||
if (isStale) {
|
||||
console.time('rundownCache__init');
|
||||
init(getPersistedRundown());
|
||||
generate();
|
||||
console.timeEnd('rundownCache__init');
|
||||
}
|
||||
return {
|
||||
@@ -117,21 +160,25 @@ type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingRetur
|
||||
*/
|
||||
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||
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<M
|
||||
throw new Error('Invalid event type');
|
||||
}
|
||||
|
||||
// @ts-expect-error -- testing
|
||||
console.log('patch', formatFromMillis(patch?.timeStart ?? 0, 'HH:mm:ss'));
|
||||
|
||||
const eventInMemory = persistedRundown[indexAt];
|
||||
const newEvent = makeEvent(eventInMemory, patch);
|
||||
|
||||
const newRundown = [...persistedRundown];
|
||||
newRundown[indexAt] = newEvent;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ensureDirectory } from './utils/fileManagement.js';
|
||||
|
||||
/**
|
||||
* @description Returns public path depending on OS
|
||||
* This is the correct path for the app running in production mode
|
||||
*/
|
||||
export function getAppDataPath(): string {
|
||||
// handle docker
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ProjectData,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimeStrategy,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
@@ -33,6 +34,8 @@ describe('test json parser with valid def', () => {
|
||||
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<number>(validated.timeStart);
|
||||
assertType<number>(validated.timeEnd);
|
||||
assertType<number>(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', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<DatabaseModel>): Promise<Datab
|
||||
return returnData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function infers strategy for a patch with only partial timer data
|
||||
* @param end
|
||||
* @param duration
|
||||
* @param fallback
|
||||
* @returns
|
||||
*/
|
||||
function inferStrategy(end: unknown, duration: unknown, fallback: TimeStrategy): TimeStrategy {
|
||||
if (end && !duration) {
|
||||
return TimeStrategy.LockEnd;
|
||||
}
|
||||
|
||||
if (!end && duration) {
|
||||
return TimeStrategy.LockDuration;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): 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<Onti
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
timeStrategy,
|
||||
linkStart: validateLinkStart(maybeLinkStart),
|
||||
endAction: validateEndAction(patchEvent.endAction, EndAction.None),
|
||||
timerType: validateTimerType(patchEvent.timerType, TimerType.CountDown),
|
||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||
@@ -374,8 +398,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
||||
// short circuit empty string
|
||||
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
||||
revision: originalEvent.revision,
|
||||
timeWarning: patchEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger,
|
||||
timeWarning: patchEvent.timeWarning ?? originalEvent.timeWarning,
|
||||
timeDanger: patchEvent.timeDanger ?? originalEvent.timeDanger,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import { join } from 'path';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { getAppDataPath } from '../setup.js';
|
||||
import { ensureDirectory } from './fileManagement.js';
|
||||
import { cellRequestFromEvent, getA1Notation } from './sheetUtils.js';
|
||||
import { parseExcel } from './parser.js';
|
||||
import { parseRundown, parseUserFields } from './parserFunctions.js';
|
||||
import { getRundown } from '../services/rundown-service/RundownService.js';
|
||||
|
||||
type ResponseOK = {
|
||||
data: Partial<DatabaseModel>;
|
||||
@@ -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<sheets_v4.Schema$Request>();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum TimeStrategy {
|
||||
LockEnd = 'lock-end',
|
||||
LockDuration = 'lock-duration',
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Generated
+248
-83
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user