mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 22:47:59 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9f0bdef4d | |||
| 8386105b75 | |||
| 459d584b35 | |||
| f98af929b3 | |||
| cbabec2b1d | |||
| 959a2b6d4a |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "3.12.0",
|
||||
"version": "3.11.1-beta.1",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "3.12.0",
|
||||
"version": "3.11.1-beta.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -31,7 +31,7 @@
|
||||
"react-qr-code": "^2.0.12",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"web-vitals": "^3.1.1",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||
|
||||
@@ -14,6 +14,7 @@ interface ExternalLinkProps {
|
||||
|
||||
export default function ExternalLink(props: ExternalLinkProps) {
|
||||
const { href, inline, children } = props;
|
||||
const classes = cx([style.link, inline ? style.inline : null]);
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -21,14 +22,8 @@ export default function ExternalLink(props: ExternalLinkProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href='#!'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={cx([style.link, inline && style.inline])}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children} <IoOpenOutline style={{ fontSize: '1em' }} />
|
||||
<a href='#!' target='_blank' rel='noreferrer' className={classes} onClick={handleClick}>
|
||||
{children} <IoOpenOutline />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $element-spacing;
|
||||
|
||||
background-color: $gray-1200;
|
||||
border-radius: 3px;
|
||||
font-size: $text-body-size;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.content {
|
||||
color: $gray-200;
|
||||
}
|
||||
background-color: $gray-1100;
|
||||
border-radius: 2px;
|
||||
font-size: $inner-section-text-size;
|
||||
|
||||
svg {
|
||||
align-self: start;
|
||||
font-size: 1.5rem;
|
||||
color: $info-blue;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './Info.module.scss';
|
||||
|
||||
interface InfoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Info(props: PropsWithChildren<InfoProps>) {
|
||||
const { className, children } = props;
|
||||
|
||||
export default function Info({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className={cx([style.infoLabel, className])}>
|
||||
<div className={style.infoLabel}>
|
||||
<IoAlertCircle />
|
||||
<div>{children}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useController, UseControllerProps } from 'react-hook-form';
|
||||
import { IoEyedrop } from '@react-icons/all-files/io5/IoEyedrop';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { debounce } from '../../../utils/debounce';
|
||||
import PopoverPicker from '../../../../common/components/input/popover-picker/PopoverPicker';
|
||||
import { debounce } from '../../../../common/utils/debounce';
|
||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
||||
import PopoverPicker from '../popover-picker/PopoverPicker';
|
||||
|
||||
import style from './SwatchSelect.module.scss';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../hooks/useFadeOutOnInactivity';
|
||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -9,6 +9,10 @@ $progress-bar-br: 3px;
|
||||
border-radius: $progress-bar-br;
|
||||
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||
overflow: clip;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar__indicator {
|
||||
|
||||
@@ -7,15 +7,16 @@ import './ProgressBar.scss';
|
||||
interface ProgressBarProps {
|
||||
current: MaybeNumber;
|
||||
duration: MaybeNumber;
|
||||
hidden?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProgressBar(props: ProgressBarProps) {
|
||||
const { current, duration, className } = props;
|
||||
const { current, duration, hidden, className = '' } = props;
|
||||
const progress = getProgress(current, duration);
|
||||
|
||||
return (
|
||||
<div className={`progress-bar__bg ${className}`}>
|
||||
<div className={`progress-bar__bg ${hidden ? 'progress-bar__bg--hidden' : ''} ${className}`}>
|
||||
<div className='progress-bar__indicator' style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.empty {
|
||||
width: 100%;
|
||||
opacity: 0.8;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
||||
@@ -16,7 +16,3 @@
|
||||
overflow-y: scroll;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||
import Info from '../info/Info';
|
||||
|
||||
import { ViewOption } from './types';
|
||||
@@ -132,9 +132,7 @@ export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
{viewSettings.overrideStyles && (
|
||||
<Info className={style.info}>This view style is being modified by a custom CSS file.</Info>
|
||||
)}
|
||||
{viewSettings.overrideStyles && <Info>This view style is being modified by a custom CSS file.</Info>}
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit} className={style.sectionList}>
|
||||
{viewOptions.map((section) => (
|
||||
<ViewParamsSection
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
import { useClientStore } from '../stores/clientStore';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
@@ -10,12 +9,10 @@ import { useIsOnline } from './useSocket';
|
||||
export const useClientPath = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname, search } = useLocation();
|
||||
const { redirect, setRedirect } = useClientStore(
|
||||
useShallow((store) => ({
|
||||
redirect: store.redirect,
|
||||
setRedirect: store.setRedirect,
|
||||
})),
|
||||
);
|
||||
const { redirect, setRedirect } = useClientStore((store) => ({
|
||||
redirect: store.redirect,
|
||||
setRedirect: store.setRedirect,
|
||||
}));
|
||||
const isOnline = useIsOnline();
|
||||
|
||||
// notify of client path changes
|
||||
|
||||
@@ -55,17 +55,6 @@ export const useEventAction = () => {
|
||||
defaultEndAction,
|
||||
} = useEditorSettings();
|
||||
|
||||
const getEventById = useCallback(
|
||||
(eventId: string) => {
|
||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||
if (!cachedRundown?.rundown) {
|
||||
return;
|
||||
}
|
||||
return cachedRundown.rundown[eventId];
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to add new event
|
||||
* @private
|
||||
@@ -626,7 +615,6 @@ export const useEventAction = () => {
|
||||
batchUpdateEvents,
|
||||
deleteEvent,
|
||||
deleteAllEvents,
|
||||
getEventById,
|
||||
reorderEvent,
|
||||
swapEvents,
|
||||
updateEvent,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { throttle } from '../utils/throttle';
|
||||
|
||||
import { throttle } from '../../common/utils/throttle';
|
||||
|
||||
export const useFadeOutOnInactivity = () => {
|
||||
const [isMouseMoved, setIsMouseMoved] = useState(false);
|
||||
|
||||
@@ -3,54 +3,73 @@ import { RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ont
|
||||
import { useRuntimeStore } from '../stores/runtime';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
|
||||
const createSelector =
|
||||
<T>(selector: (state: RuntimeStore) => T) =>
|
||||
() =>
|
||||
useRuntimeStore(selector);
|
||||
|
||||
export const setClientRemote = {
|
||||
setIdentify: (payload: { target: string; identify: boolean }) => socketSendJson('client', payload),
|
||||
setRedirect: (payload: { target: string; redirect: string }) => socketSendJson('client', payload),
|
||||
setClientName: (payload: { target: string; rename: string }) => socketSendJson('client', payload),
|
||||
};
|
||||
|
||||
export const useRundownEditor = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
nextEventId: state.eventNext?.id ?? null,
|
||||
}));
|
||||
export const useRundownEditor = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
nextEventId: state.eventNext?.id ?? null,
|
||||
});
|
||||
|
||||
export const useOperator = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useTimerViewControl = createSelector((state: RuntimeStore) => ({
|
||||
blackout: state.message.timer.blackout,
|
||||
blink: state.message.timer.blink,
|
||||
secondarySource: state.message.timer.secondarySource,
|
||||
}));
|
||||
export const useOperator = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
});
|
||||
|
||||
export const useTimerMessageInput = createSelector((state: RuntimeStore) => ({
|
||||
text: state.message.timer.text,
|
||||
visible: state.message.timer.visible,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useExternalMessageInput = createSelector((state: RuntimeStore) => ({
|
||||
text: state.message.external,
|
||||
visible: state.message.timer.secondarySource === 'external',
|
||||
}));
|
||||
export const useTimerViewControl = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
blackout: state.message.timer.blackout,
|
||||
blink: state.message.timer.blink,
|
||||
secondarySource: state.message.timer.secondarySource,
|
||||
});
|
||||
|
||||
export const useMessagePreview = createSelector((state: RuntimeStore) => ({
|
||||
blink: state.message.timer.blink,
|
||||
blackout: state.message.timer.blackout,
|
||||
phase: state.timer.phase,
|
||||
showAuxTimer: state.message.timer.secondarySource === 'aux',
|
||||
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
|
||||
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
|
||||
timerType: state.eventNow?.timerType ?? null,
|
||||
countToEnd: state.eventNow?.countToEnd ?? false,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useTimerMessageInput = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
text: state.message.timer.text,
|
||||
visible: state.message.timer.visible,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useExternalMessageInput = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
text: state.message.external,
|
||||
visible: state.message.timer.secondarySource === 'external',
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useMessagePreview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
blink: state.message.timer.blink,
|
||||
blackout: state.message.timer.blackout,
|
||||
phase: state.timer.phase,
|
||||
showAuxTimer: state.message.timer.secondarySource === 'aux',
|
||||
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
|
||||
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
|
||||
timerType: state.eventNow?.timerType ?? null,
|
||||
countToEnd: state.eventNow?.countToEnd ?? false,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const setMessage = {
|
||||
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
||||
@@ -62,12 +81,16 @@ export const setMessage = {
|
||||
socketSendJson('message', { timer: { secondarySource: payload } }),
|
||||
};
|
||||
|
||||
export const usePlaybackControl = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
timerPhase: state.timer.phase,
|
||||
}));
|
||||
export const usePlaybackControl = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
timerPhase: state.timer.phase,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const setPlayback = {
|
||||
start: () => socketSendJson('start'),
|
||||
@@ -91,20 +114,32 @@ export const setPlayback = {
|
||||
},
|
||||
};
|
||||
|
||||
export const useInfoPanel = createSelector((state: RuntimeStore) => ({
|
||||
eventNow: state.eventNow,
|
||||
eventNext: state.eventNext,
|
||||
playback: state.timer.playback,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
}));
|
||||
export const useInfoPanel = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
eventNow: state.eventNow,
|
||||
eventNext: state.eventNext,
|
||||
playback: state.timer.playback,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
});
|
||||
|
||||
export const useAuxTimerTime = createSelector((state: RuntimeStore) => state.auxtimer1.current);
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.auxtimer1.playback,
|
||||
direction: state.auxtimer1.direction,
|
||||
}));
|
||||
export const useAuxTimerTime = () => {
|
||||
const featureSelector = (state: RuntimeStore) => state.auxtimer1.current;
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useAuxTimerControl = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.auxtimer1.playback,
|
||||
direction: state.auxtimer1.direction,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const setAuxTimer = {
|
||||
start: () => socketSendJson('auxtimer', { '1': SimplePlayback.Start }),
|
||||
@@ -114,13 +149,20 @@ export const setAuxTimer = {
|
||||
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
||||
};
|
||||
|
||||
export const useSelectedEventId = createSelector((state: RuntimeStore) => ({
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
}));
|
||||
export const useSelectedEventId = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
});
|
||||
|
||||
export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({
|
||||
currentBlockId: state.currentBlock.block?.id ?? null,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useCurrentBlockId = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
currentBlockId: state.currentBlock.block?.id ?? null,
|
||||
});
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const setEventPlayback = {
|
||||
loadEvent: (id: string) => socketSendJson('load', { id }),
|
||||
@@ -129,56 +171,90 @@ export const setEventPlayback = {
|
||||
pause: () => socketSendJson('pause'),
|
||||
};
|
||||
|
||||
export const useTimer = createSelector((state: RuntimeStore) => ({
|
||||
...state.timer,
|
||||
}));
|
||||
export const useTimer = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
...state.timer,
|
||||
});
|
||||
|
||||
export const useClock = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useClock = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
});
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
/** Used by the progress bar components */
|
||||
export const useProgressData = createSelector((state: RuntimeStore) => ({
|
||||
current: state.timer.current,
|
||||
duration: state.timer.duration,
|
||||
timeWarning: state.eventNow?.timeWarning ?? null,
|
||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||
}));
|
||||
export const useProgressData = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
current: state.timer.current,
|
||||
duration: state.timer.duration,
|
||||
timeWarning: state.eventNow?.timeWarning ?? null,
|
||||
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||
});
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
|
||||
|
||||
export const useRuntimeOverview = createSelector((state: RuntimeStore) => ({
|
||||
plannedStart: state.runtime.plannedStart,
|
||||
actualStart: state.runtime.actualStart,
|
||||
plannedEnd: state.runtime.plannedEnd,
|
||||
expectedEnd: state.runtime.expectedEnd,
|
||||
}));
|
||||
export const useRuntimeOverview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
plannedStart: state.runtime.plannedStart,
|
||||
actualStart: state.runtime.actualStart,
|
||||
plannedEnd: state.runtime.plannedEnd,
|
||||
expectedEnd: state.runtime.expectedEnd,
|
||||
});
|
||||
|
||||
export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
clock: state.clock,
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
numEvents: state.runtime.numEvents,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
offset: state.runtime.offset,
|
||||
export const useRuntimePlaybackOverview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
clock: state.clock,
|
||||
|
||||
currentBlock: state.currentBlock,
|
||||
}));
|
||||
numEvents: state.runtime.numEvents,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
offset: state.runtime.offset,
|
||||
|
||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
offset: state.runtime.offset,
|
||||
}));
|
||||
currentBlock: state.currentBlock,
|
||||
});
|
||||
|
||||
export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({
|
||||
offset: state.runtime.offset,
|
||||
}));
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const usePing = createSelector((state: RuntimeStore) => ({
|
||||
ping: state.ping,
|
||||
}));
|
||||
export const useTimelineStatus = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
clock: state.clock,
|
||||
offset: state.runtime.offset,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useRuntimeOffset = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
offset: state.runtime.offset,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const usePing = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
ping: state.ping,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
/** convert ping into a derived value which changes less often */
|
||||
export const useIsOnline = createSelector((state: RuntimeStore) => ({
|
||||
isOnline: state.ping > 0,
|
||||
}));
|
||||
export const useIsOnline = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
isOnline: state.ping > 0,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
+4
-14
@@ -27,8 +27,7 @@
|
||||
.titleSection,
|
||||
.filterSection,
|
||||
.oscSection,
|
||||
.httpSection,
|
||||
.actionSection {
|
||||
.httpSection {
|
||||
display: grid;
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
@@ -41,10 +40,8 @@
|
||||
.ruleSection,
|
||||
.filterSection,
|
||||
.oscSection,
|
||||
.httpSection,
|
||||
.actionSection {
|
||||
label,
|
||||
div {
|
||||
.httpSection {
|
||||
label, div {
|
||||
// we use the div as non-interactive placeholder for button cells
|
||||
// it needs to match the size of the label element
|
||||
font-size: calc(1rem - 3px);
|
||||
@@ -54,6 +51,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.titleSection {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -70,14 +68,6 @@
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.actionSection {
|
||||
grid-template-columns: auto 1fr 1fr auto;
|
||||
|
||||
.test {
|
||||
grid-column: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.outputCard {
|
||||
border-left: 0.25rem solid $gray-1200;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
+102
-150
@@ -1,23 +1,23 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import {
|
||||
Automation,
|
||||
AutomationDTO,
|
||||
HTTPOutput,
|
||||
isHTTPOutput,
|
||||
isOntimeAction,
|
||||
isOSCOutput,
|
||||
OntimeAction,
|
||||
OSCOutput,
|
||||
} from 'ontime-types';
|
||||
import { Automation, AutomationDTO, HTTPOutput, isHTTPOutput, isOSCOutput, OSCOutput } from 'ontime-types';
|
||||
|
||||
import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||
@@ -27,7 +27,6 @@ import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import TemplateInput from './template-input/TemplateInput';
|
||||
import { isAutomation, makeFieldList } from './automationUtils';
|
||||
import OntimeActionForm from './OntimeActionForm';
|
||||
|
||||
import style from './AutomationForm.module.scss';
|
||||
|
||||
@@ -52,7 +51,6 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
register,
|
||||
setError,
|
||||
setFocus,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<AutomationDTO>({
|
||||
mode: 'onChange',
|
||||
@@ -103,11 +101,6 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
appendOutput({ type: 'http', url: '' });
|
||||
};
|
||||
|
||||
const handleAddnewOntimeAction = () => {
|
||||
// @ts-expect-error -- we dont want to choose an action
|
||||
appendOutput({ type: 'ontime', action: undefined });
|
||||
};
|
||||
|
||||
const handleTestOSCOutput = async (index: number) => {
|
||||
try {
|
||||
const values = getValues(`outputs.${index}`) as OSCOutput;
|
||||
@@ -141,19 +134,6 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestOntimeAction = async (index: number) => {
|
||||
try {
|
||||
const values = getValues(`outputs.${index}`) as OntimeAction;
|
||||
// NOTE: there is no meaningful validation to do here, we let the server deal with the data
|
||||
await testOutput({
|
||||
...values,
|
||||
type: 'ontime',
|
||||
});
|
||||
} catch (_error) {
|
||||
/** we dont handle errors here */
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: AutomationDTO) => {
|
||||
if (isAutomation(automation)) {
|
||||
await handleEdit(automation.id, { id: automation.id, ...values });
|
||||
@@ -210,7 +190,7 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
</div>
|
||||
|
||||
<div className={style.innerSection}>
|
||||
<h3>Filters (optional)</h3>
|
||||
<h3>Filters</h3>
|
||||
<div className={style.ruleSection}>
|
||||
<label>
|
||||
Trigger outputs if
|
||||
@@ -225,80 +205,74 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
{fieldFilters.map((field, index) => {
|
||||
const key = `filters.${index}.field.${field.id}`;
|
||||
return (
|
||||
<div key={key} className={style.filterSection}>
|
||||
<label>
|
||||
Runtime data source
|
||||
<Select
|
||||
{...register(`filters.${index}.field`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Event field
|
||||
{fieldFilters.map((field, index) => (
|
||||
<div key={field.id} className={style.filterSection}>
|
||||
<label>
|
||||
Runtime data source
|
||||
<Select
|
||||
{...register(`filters.${index}.field`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Event field
|
||||
</option>
|
||||
{fieldList.map(({ value, label }) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
{fieldList.map(({ value, label }, localIndex) => {
|
||||
const key = `filters.${index}.field.${localIndex}`;
|
||||
return (
|
||||
<option key={key} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Matching condition
|
||||
<Select
|
||||
{...register(`filters.${index}.operator`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Operator
|
||||
</option>
|
||||
<option value='equals'>equals</option>
|
||||
<option value='not_equals'>not equals</option>
|
||||
<option value='contains'>contains</option>
|
||||
{/*
|
||||
))}
|
||||
</Select>
|
||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Matching condition
|
||||
<Select
|
||||
{...register(`filters.${index}.operator`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Operator
|
||||
</option>
|
||||
<option value='equals'>equals</option>
|
||||
<option value='not_equals'>not equals</option>
|
||||
<option value='contains'>contains</option>
|
||||
{/*
|
||||
We dont currently offer a data source where these operators would make sense
|
||||
<option value='greater_than'>greater than</option>
|
||||
<option value='less_than'>less than</option>
|
||||
*/}
|
||||
</Select>
|
||||
<Panel.Error>{errors.filters?.[index]?.operator?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Value to match
|
||||
<Input
|
||||
{...register(`filters.${index}.value`)}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='<empty / no value>'
|
||||
autoComplete='off'
|
||||
/>
|
||||
</label>
|
||||
</Select>
|
||||
<Panel.Error>{errors.filters?.[index]?.operator?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Value to match
|
||||
<Input
|
||||
{...register(`filters.${index}.value`)}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='<empty / no value>'
|
||||
autoComplete='off'
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<span> </span>
|
||||
<div>
|
||||
<span> </span>
|
||||
<div>
|
||||
<IconButton
|
||||
aria-label='Delete'
|
||||
icon={<IoTrash />}
|
||||
variant='ontime-ghosted'
|
||||
size='sm'
|
||||
color='#FA5656' // $red-500
|
||||
onClick={() => removeFilter(index)}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-label='Delete'
|
||||
icon={<IoTrash />}
|
||||
variant='ontime-ghosted'
|
||||
size='sm'
|
||||
color='#FA5656' // $red-500
|
||||
onClick={() => removeFilter(index)}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
@@ -317,10 +291,13 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
|
||||
<div className={style.innerColumn}>
|
||||
<h3>Outputs</h3>
|
||||
<Info>
|
||||
Automation outputs can be used to send data from Ontime to external software.
|
||||
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Automation outputs can be used to send data from Ontime to external software.
|
||||
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{fieldOutputs.map((output, index) => {
|
||||
if (isOSCOutput(output)) {
|
||||
@@ -380,13 +357,13 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
<Panel.Error>{rowErrors?.address?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Arguments
|
||||
Parameters
|
||||
<TemplateInput
|
||||
{...register(`outputs.${index}.args`)}
|
||||
value={output.args}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='1'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Panel.Error>{rowErrors?.args?.message}</Panel.Error>
|
||||
</label>
|
||||
@@ -403,6 +380,8 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
size='sm'
|
||||
onClick={() => removeOutput(index)}
|
||||
color='#FA5656' // $red-500
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
@@ -450,6 +429,8 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
size='sm'
|
||||
onClick={() => removeOutput(index)}
|
||||
color='#FA5656' // $red-500
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
@@ -457,59 +438,30 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOntimeAction(output)) {
|
||||
const rowErrors = errors.outputs?.[index] as
|
||||
| {
|
||||
action?: { message?: string };
|
||||
time?: { message?: string };
|
||||
text?: { message?: string };
|
||||
visible?: { message?: string };
|
||||
secondarySource?: { message?: string };
|
||||
}
|
||||
| undefined;
|
||||
return (
|
||||
<div key={output.id} className={style.outputCard}>
|
||||
<Tag>Ontime action</Tag>
|
||||
<OntimeActionForm
|
||||
value={output.action}
|
||||
index={index}
|
||||
register={register}
|
||||
rowErrors={rowErrors}
|
||||
setValue={setValue}
|
||||
>
|
||||
<span> </span>
|
||||
<Panel.InlineElements relation='inner'>
|
||||
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestOntimeAction(index)}>
|
||||
Test
|
||||
</Button>
|
||||
<IconButton
|
||||
aria-label='Delete'
|
||||
icon={<IoTrash />}
|
||||
variant='ontime-ghosted'
|
||||
size='sm'
|
||||
onClick={() => removeOutput(index)}
|
||||
color='#FA5656' // $red-500
|
||||
/>
|
||||
</Panel.InlineElements>
|
||||
</OntimeActionForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// there should be no other output types
|
||||
return null;
|
||||
})}
|
||||
<Panel.InlineElements relation='inner'>
|
||||
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewOSCOutput}>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
rightIcon={<IoAdd />}
|
||||
size='sm'
|
||||
onClick={handleAddNewOSCOutput}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
>
|
||||
OSC
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewHTTPOutput}>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
rightIcon={<IoAdd />}
|
||||
size='sm'
|
||||
onClick={handleAddNewHTTPOutput}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddnewOntimeAction}>
|
||||
Ontime action
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
|
||||
|
||||
+9
-12
@@ -1,13 +1,11 @@
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button, Input, Switch } from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertIcon, Button, Input, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { editAutomationSettings } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
const oscApiDocsUrl = 'https://docs.getontime.no/api/protocols/osc/';
|
||||
@@ -77,13 +75,14 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
||||
<Panel.Divider />
|
||||
|
||||
<Panel.Section>
|
||||
<Info>
|
||||
<p>Control Ontime and share its data with external systems in your workflow.</p>
|
||||
<p>- Automations allow Ontime to send its data on lifecycle triggers.</p>
|
||||
<p>- OSC Input tells Ontime to listen to messages on the specific port.</p>
|
||||
<br />
|
||||
<ExternalLink href={oscApiDocsUrl}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Control Ontime and share its data with external systems in your workflow. <br />
|
||||
- Automations allow Ontime to send its data on lifecycle triggers. <br />- OSC Input tells Ontime to listen
|
||||
to messages on the specific port. <ExternalLink href={oscApiDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Panel.Section>
|
||||
|
||||
<Panel.Section
|
||||
@@ -113,9 +112,7 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.Title>OSC Input</Panel.Title>
|
||||
|
||||
<Panel.ListGroup>
|
||||
{isOntimeCloud && <Info>For security reasons OSC integrations are not available in the cloud service.</Info>}
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='OSC input'
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
|
||||
import { Input, Select } from '@chakra-ui/react';
|
||||
import { AutomationDTO, OntimeAction } from 'ontime-types';
|
||||
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import style from './AutomationForm.module.scss';
|
||||
|
||||
interface OntimeActionFormProps {
|
||||
index: number;
|
||||
register: UseFormRegister<AutomationDTO>;
|
||||
rowErrors?: {
|
||||
action?: { message?: string };
|
||||
time?: { message?: string };
|
||||
text?: { message?: string };
|
||||
visible?: { message?: string };
|
||||
secondarySource?: { message?: string };
|
||||
};
|
||||
value: OntimeAction['action'];
|
||||
setValue: UseFormSetValue<AutomationDTO>;
|
||||
}
|
||||
|
||||
export default function OntimeActionForm(props: PropsWithChildren<OntimeActionFormProps>) {
|
||||
const { index, register, setValue, rowErrors, value, children } = props;
|
||||
const [selectedAction, setSelectedAction] = useState<OntimeAction['action']>(value || 'aux-start');
|
||||
|
||||
const updateSelectedAction = (value: string) => {
|
||||
setSelectedAction(value as OntimeAction['action']);
|
||||
setValue(`outputs.${index}.action`, value as OntimeAction['action']);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx([style.actionSection, selectedAction && style[selectedAction]])}>
|
||||
<input type='hidden' {...register(`outputs.${index}.action`)} value={selectedAction} />
|
||||
<label>
|
||||
Action
|
||||
<Select
|
||||
variant='ontime'
|
||||
size='sm'
|
||||
value={selectedAction}
|
||||
onChange={(event) => updateSelectedAction(event.target.value)}
|
||||
>
|
||||
<option value='aux-start'>Auxiliary timer: start</option>
|
||||
<option value='aux-pause'>Auxiliary timer: pause</option>
|
||||
<option value='aux-stop'>Auxiliary timer: stop</option>
|
||||
<option value='aux-set'>Auxiliary timer: set</option>
|
||||
<option value='message-set'>Timer: timer message</option>
|
||||
<option value='message-secondary'>Timer: timer secondary</option>
|
||||
</Select>
|
||||
<Panel.Error>{rowErrors?.action?.message}</Panel.Error>
|
||||
</label>
|
||||
|
||||
{selectedAction === 'aux-set' && (
|
||||
<label>
|
||||
New time
|
||||
<Input
|
||||
{...register(`outputs.${index}.time`, {
|
||||
required: { value: true, message: 'Required field' },
|
||||
})}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='eg: 10m5s'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Panel.Error>{rowErrors?.time?.message}</Panel.Error>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{selectedAction === 'message-set' && (
|
||||
<>
|
||||
<label>
|
||||
Text (leave empty for no change)
|
||||
<Input
|
||||
{...register(`outputs.${index}.text`)}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='eg: Timer is finished'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Visibility
|
||||
<Select variant='ontime' size='sm' {...register(`outputs.${index}.visible`)}>
|
||||
<option value=''>Untouched</option>
|
||||
<option value='true'>Show</option>
|
||||
<option value='false'>Hide</option>
|
||||
</Select>
|
||||
<Panel.Error>{rowErrors?.visible?.message}</Panel.Error>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAction === 'message-secondary' && (
|
||||
<label>
|
||||
Timer secondary source
|
||||
<Select variant='ontime' size='sm' {...register(`outputs.${index}.secondarySource`)}>
|
||||
<option value='aux'>Auxiliary timer</option>
|
||||
<option value='external'>External</option>
|
||||
<option value='null'>None</option>
|
||||
</Select>
|
||||
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
||||
</label>
|
||||
)}
|
||||
<div className={style.test}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { type InputProps, Input } from '@chakra-ui/react';
|
||||
import { mergeRefs, useClickOutside } from '@mantine/hooks';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
|
||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||
|
||||
@@ -10,10 +10,10 @@ import style from './TemplateInput.module.scss';
|
||||
|
||||
interface TemplateInputProps extends InputProps {}
|
||||
|
||||
const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProps, ref) {
|
||||
export default function TemplateInput(props: TemplateInputProps) {
|
||||
const { value, onChange, ...rest } = props;
|
||||
const { data } = useCustomFields();
|
||||
const localRef = useClickOutside(() => setShowSuggestions(false));
|
||||
const ref = useClickOutside(() => setShowSuggestions(false));
|
||||
|
||||
const autocompleteList = useMemo(() => {
|
||||
return makeAutoCompleteList(data);
|
||||
@@ -31,13 +31,15 @@ const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProp
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
|
||||
if (event.target.value.endsWith('{')) {
|
||||
if (event.target.value.endsWith('{{')) {
|
||||
setShowSuggestions(true);
|
||||
setSuggestions(updateSuggestions(event.target.value));
|
||||
} else if (event.target.value === '' || event.target.value.endsWith('}}')) {
|
||||
setShowSuggestions(false);
|
||||
} else if (showSuggestions) {
|
||||
setSuggestions(updateSuggestions(event.target.value));
|
||||
}
|
||||
|
||||
if (showSuggestions) {
|
||||
const suggestions = updateSuggestions(event.target.value);
|
||||
setSuggestions(suggestions);
|
||||
}
|
||||
|
||||
onChange?.(event);
|
||||
@@ -52,8 +54,8 @@ const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProp
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.wrapper} ref={mergeRefs(localRef, ref)}>
|
||||
<Input value={inputValue} {...rest} onChange={handleInputChange} autoComplete='off' autoCorrect='off' />
|
||||
<div className={style.wrapper} ref={ref}>
|
||||
<Input {...rest} value={inputValue} onChange={handleInputChange} autoComplete='off' autoCorrect='off' />
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<ul className={style.suggestions}>
|
||||
{suggestions.map((suggestion) => (
|
||||
@@ -65,6 +67,4 @@ const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProp
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TemplateInput;
|
||||
}
|
||||
|
||||
+4
-2
@@ -6,8 +6,10 @@ describe('matchRemaining()', () => {
|
||||
expect(matchRemaining('{{{hum', '{{human}}')).toBe('an}}');
|
||||
expect(matchRemaining('send {', '{{human}}')).toBe('{human}}');
|
||||
|
||||
expect(matchRemaining('{', '{{human}}')).toBe('{human}}');
|
||||
expect(matchRemaining('{{', '{{human}}')).toBe('human}}');
|
||||
// we should be able to match the following
|
||||
// however, the current implementation only needs to deal with strings that start with {{
|
||||
// expect(matchRemaining('{', '{{human}}')).toBe('{human}}');
|
||||
// expect(matchRemaining('{{', '{{human}}')).toBe('human}}');
|
||||
});
|
||||
|
||||
it('should return an empty string if there are no matches or if it is complete', () => {
|
||||
|
||||
-10
@@ -83,16 +83,6 @@ export function matchRemaining(a: string, b: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// naive match assuming that a template will start with {{
|
||||
if (a.endsWith('{{') && b.startsWith('{{')) {
|
||||
return b.substring(2);
|
||||
}
|
||||
|
||||
// naive match assuming that a template will start with {
|
||||
if (a.endsWith('{') && b.startsWith('{{')) {
|
||||
return b.substring(1);
|
||||
}
|
||||
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
const searchString = b.substring(0, i + 1);
|
||||
if (a.endsWith(searchString)) {
|
||||
|
||||
+27
-25
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertIcon, Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
@@ -10,7 +10,6 @@ import { postUrlPresets } from '../../../../common/api/urlPresets';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||
@@ -109,29 +108,32 @@ export default function UrlPresetsForm() {
|
||||
</Panel.InlineElements>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Info>
|
||||
URL presets are user defined aliases to Ontime URLs
|
||||
<br />
|
||||
<br />
|
||||
<b>Preset Name</b> <br />
|
||||
The alias for the URL. This will be the URL you will be calling. eg: <br />
|
||||
<Panel.BlockQuote>
|
||||
Preset name <Panel.Highlight>cam3</Panel.Highlight> called as{' '}
|
||||
<Panel.Highlight>http://localhost:4001/cam3</Panel.Highlight>
|
||||
</Panel.BlockQuote>
|
||||
<br />
|
||||
<b>URL Segment</b> <br />
|
||||
The corresponding alias path and configuration parameters. eg: <br />
|
||||
<Panel.BlockQuote>
|
||||
URL segment <Panel.Highlight>backstage?hidePast=true&stopCycle=true</Panel.Highlight> corresponds to
|
||||
complete URL
|
||||
<Panel.Highlight>http://localhost:4001/backstage?hidePast=true&stopCycle=true</Panel.Highlight>
|
||||
</Panel.BlockQuote>
|
||||
<br />
|
||||
You will need to save the changes before the presets are functional.
|
||||
<br />
|
||||
<ExternalLink href={urlPresetsDocs}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
URL presets are user defined aliases to Ontime URLs
|
||||
<br />
|
||||
<br />
|
||||
<b>Preset Name</b> <br />
|
||||
The alias for the URL. This will be the URL you will be calling. eg: <br />
|
||||
<Panel.BlockQuote>
|
||||
Preset name <Panel.Highlight>cam3</Panel.Highlight> called as{' '}
|
||||
<Panel.Highlight>http://localhost:4001/cam3</Panel.Highlight>
|
||||
</Panel.BlockQuote>
|
||||
<br />
|
||||
<b>URL Segment</b> <br />
|
||||
The corresponding alias path and configuration parameters. eg: <br />
|
||||
<Panel.BlockQuote>
|
||||
URL segment <Panel.Highlight>backstage?hidePast=true&stopCycle=true</Panel.Highlight> corresponds to
|
||||
complete URL
|
||||
<Panel.Highlight>http://localhost:4001/backstage?hidePast=true&stopCycle=true</Panel.Highlight>
|
||||
</Panel.BlockQuote>
|
||||
<br />
|
||||
You will need to save the changes before the presets are functional.
|
||||
<br />
|
||||
<ExternalLink href={urlPresetsDocs}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Panel.Section>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.Title>
|
||||
|
||||
+15
-13
@@ -1,11 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||
|
||||
import { deleteCustomField, editCustomField, postCustomField } from '../../../../../common/api/customFields';
|
||||
import ExternalLink from '../../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../../common/components/info/Info';
|
||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||
import { customFieldsDocsUrl } from '../../../../../externals';
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
@@ -56,17 +55,20 @@ export default function CustomFields() {
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
<Info>
|
||||
Custom fields allow for additional information to be added to an event.
|
||||
<br />
|
||||
<br />
|
||||
This data is not used by Ontime, but provides place for cueing or department specific information (eg.
|
||||
light, sound, camera).
|
||||
<br />
|
||||
<br />
|
||||
Custom fields can be used width the Integrations feature using the generated key.
|
||||
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Custom fields allow for additional information to be added to an event.
|
||||
<br />
|
||||
<br />
|
||||
This data is not used by Ontime, but provides place for cueing or department specific information (eg.
|
||||
light, sound, camera).
|
||||
<br />
|
||||
<br />
|
||||
Custom fields can be used width the Integrations feature using the generated key.
|
||||
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Panel.Section>
|
||||
<Panel.Section>
|
||||
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button, Input, Switch } from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertIcon, Button, Input, Switch } from '@chakra-ui/react';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import { postViewSettings } from '../../../../common/api/viewSettings';
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||
@@ -87,19 +85,16 @@ export default function ViewSettingsForm() {
|
||||
</Panel.InlineElements>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Info>
|
||||
You can the Ontime views or customise its styles by modifying the provided CSS file.
|
||||
<br />
|
||||
{!isOntimeCloud && (
|
||||
<>
|
||||
<br />
|
||||
The loaded CSS file is in the user directory at{' '}
|
||||
<Panel.BlockQuote>{`${info.publicDir}/user/styles/override.css`}</Panel.BlockQuote>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
You can the Ontime views or customise its styles by modifying the provided CSS file. <br />
|
||||
The CSS file is in the user directory at {`${info.publicDir}/user/styles/override.css`}
|
||||
<br />
|
||||
<br />
|
||||
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Panel.Section>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.ListGroup>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import EditorSettingsForm from './EditorSettingsForm';
|
||||
|
||||
export default function InterfacePanel() {
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Interface</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Interface settings
|
||||
<br />
|
||||
<br />
|
||||
These concern settings that are applied to this user in this browser.
|
||||
<br />
|
||||
It will not affect other users or other browsers.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Panel.Section>
|
||||
<EditorSettingsForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/';
|
||||
|
||||
export default function GSheetInfo() {
|
||||
return (
|
||||
<Info>
|
||||
Ontime allows you to synchronize your rundown with a Google Sheet.
|
||||
<br />
|
||||
<br />
|
||||
To enable this feature, you will need to generate tokens in your Google account and provide them to Ontime.
|
||||
<br />
|
||||
Once set up, you will be able to synchronize data between Ontime and your Google Sheet. <br />
|
||||
<ExternalLink href={googleSheetDocsUrl}>See the docs</ExternalLink>
|
||||
</Info>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Ontime allows you to synchronize your rundown with a Google Sheet.
|
||||
<br />
|
||||
<br />
|
||||
To enable this feature, you will need to generate tokens in your Google account and provide them to Ontime.
|
||||
<br />
|
||||
Once set up, you will be able to synchronize data between Ontime and your Google Sheet. <br />
|
||||
<ExternalLink href={googleSheetDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,14 +55,15 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const handleCancelFlow = () => {
|
||||
const handleCancelFlow = async () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets file from input
|
||||
* @param event
|
||||
*/
|
||||
const handleClientSecret = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleClientSecret = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
.eventEditor {
|
||||
max-height: 80vh;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-inline: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-inline: 0.5rem 1.5rem;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 4rem;
|
||||
|
||||
flex: 1;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
color: var(--color-override, $viewer-color);
|
||||
gap: $view-element-gap;
|
||||
padding: $view-outer-padding;
|
||||
font-size: $base-font-size;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 40vw;
|
||||
@@ -29,7 +28,6 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 25vh;
|
||||
color: var(--label-color-override, $viewer-label-color);
|
||||
}
|
||||
|
||||
/* =================== HEADER + EXTRAS ===================*/
|
||||
@@ -84,6 +82,16 @@
|
||||
gap: $view-element-gap;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: clamp(24px, 2vw, 32px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
color: var(--label-color-override, $viewer-label-color);
|
||||
}
|
||||
|
||||
.event {
|
||||
background-color: var(--card-background-color-override, $viewer-card-bg-color);
|
||||
padding: $view-card-padding;
|
||||
@@ -110,7 +118,7 @@
|
||||
max-width: 7.5em;
|
||||
}
|
||||
|
||||
.time-entry {
|
||||
.aux-timers {
|
||||
&__label {
|
||||
font-size: $timer-label-size;
|
||||
color: var(--label-color-override, $viewer-label-color);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||
import BackstageSchedule from '../common/schedule/BackstageSchedule';
|
||||
|
||||
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
||||
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
||||
@@ -68,7 +68,6 @@ export default function Backstage(props: BackstageProps) {
|
||||
}, [selectedId]);
|
||||
|
||||
// gather card data
|
||||
const hasEvents = backstageEvents.length > 0;
|
||||
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
|
||||
eventNow,
|
||||
eventNext,
|
||||
@@ -81,18 +80,16 @@ export default function Backstage(props: BackstageProps) {
|
||||
const clock = formatTime(time.clock);
|
||||
const isPendingStart = getIsPendingStart(time.playback, time.phase);
|
||||
const startedAt = isPendingStart ? formatTime(time.secondaryTimer) : formatTime(time.startedAt);
|
||||
const scheduledStart =
|
||||
hasEvents && showNow ? '' : formatTime(runtime.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' });
|
||||
const scheduledEnd =
|
||||
hasEvents && showNow ? '' : formatTime(runtime.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' });
|
||||
const scheduledStart = showNow ? '' : formatTime(runtime.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' });
|
||||
const scheduledEnd = showNow ? '' : formatTime(runtime.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' });
|
||||
|
||||
let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin });
|
||||
displayTimer = removeLeadingZero(displayTimer);
|
||||
let stageTimer = millisToString(time.current, { fallback: timerPlaceholderMin });
|
||||
stageTimer = removeLeadingZero(stageTimer);
|
||||
|
||||
// gather presentation styles
|
||||
const qrSize = Math.max(window.innerWidth / 15, 72);
|
||||
const showProgress = getShowProgressBar(time.playback);
|
||||
const showSchedule = hasEvents && screenHeight > 700; // in vertical screens we may not have space
|
||||
const showSchedule = screenHeight > 700; // in vertical screens we may not have space
|
||||
|
||||
// gather option data
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
@@ -110,64 +107,69 @@ export default function Backstage(props: BackstageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showProgress && <ProgressBar className='progress-container' current={time.current} duration={time.duration} />}
|
||||
<ProgressBar
|
||||
className='progress-container'
|
||||
current={time.current}
|
||||
duration={time.duration}
|
||||
hidden={!showProgress}
|
||||
/>
|
||||
|
||||
{!hasEvents && <Empty text={getLocalizedString('common.no_data')} className='empty-container' />}
|
||||
{backstageEvents.length === 0 && (
|
||||
<div className='empty-container'>
|
||||
<Empty text={getLocalizedString('common.no_data')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='card-container'>
|
||||
{showNow && (
|
||||
{showNow ? (
|
||||
<div className={cx(['event', 'now', blinkClass && 'blink'])}>
|
||||
<TitleCard title={nowMain} secondary={nowSecondary} />
|
||||
<div className='timer-group'>
|
||||
<div className='time-entry'>
|
||||
<div className={cx(['time-entry__label', isPendingStart && 'time-entry--pending'])}>
|
||||
<div className='aux-timers'>
|
||||
<div className={cx(['aux-timers__label', isPendingStart && 'aux-timers--pending'])}>
|
||||
{isPendingStart ? getLocalizedString('countdown.waiting') : getLocalizedString('common.started_at')}
|
||||
</div>
|
||||
<SuperscriptTime time={startedAt} className='time-entry__value' />
|
||||
<SuperscriptTime time={startedAt} className='aux-timers__value' />
|
||||
</div>
|
||||
<div className='timer-gap' />
|
||||
<div className='time-entry'>
|
||||
<div className='time-entry__label'>{getLocalizedString('common.expected_finish')}</div>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
|
||||
{isOvertime(time.current) ? (
|
||||
<div className='time-entry__value'>{getLocalizedString('countdown.overtime')}</div>
|
||||
<div className='aux-timers__value'>{getLocalizedString('countdown.overtime')}</div>
|
||||
) : (
|
||||
<SuperscriptTime time={formatTime(time.expectedFinish)} className='time-entry__value' />
|
||||
<SuperscriptTime time={formatTime(time.expectedFinish)} className='aux-timers__value' />
|
||||
)}
|
||||
</div>
|
||||
<div className='timer-gap' />
|
||||
<div className='time-entry'>
|
||||
<div className='time-entry__label'>{getLocalizedString('common.stage_timer')}</div>
|
||||
<div className='time-entry__value'>{displayTimer}</div>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
|
||||
<div className='aux-timers__value'>{stageTimer}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showNow && hasEvents && (
|
||||
) : (
|
||||
<div className='event'>
|
||||
<div className='title-card__placeholder'>{getLocalizedString('countdown.waiting')}</div>
|
||||
<div className='timer-group'>
|
||||
<div className='time-entry'>
|
||||
<div className={cx(['time-entry__label', isPendingStart && 'time-entry--pending'])}>
|
||||
<div className='aux-timers'>
|
||||
<div className={cx(['aux-timers__label', isPendingStart && 'aux-timers--pending'])}>
|
||||
{getLocalizedString('common.scheduled_start')}
|
||||
</div>
|
||||
<SuperscriptTime time={scheduledStart} className='time-entry__value' />
|
||||
<SuperscriptTime time={scheduledStart} className='aux-timers__value' />
|
||||
</div>
|
||||
<div className='timer-gap' />
|
||||
<div className='time-entry'>
|
||||
<div className='time-entry__label'>{getLocalizedString('common.scheduled_end')}</div>
|
||||
<SuperscriptTime time={scheduledEnd} className='time-entry__value' />
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>{getLocalizedString('common.scheduled_end')}</div>
|
||||
<SuperscriptTime time={scheduledEnd} className='aux-timers__value' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNext && hasEvents && (
|
||||
<TitleCard className='event' label='next' title={nextMain} secondary={nextSecondary} />
|
||||
)}
|
||||
{showNext && <TitleCard className='event' label='next' title={nextMain} secondary={nextSecondary} />}
|
||||
</div>
|
||||
|
||||
{showSchedule && <ScheduleExport selectedId={selectedId} isBackstage />}
|
||||
{showSchedule && <BackstageSchedule selectedId={selectedId} />}
|
||||
|
||||
<div className={cx(['info', !showSchedule && 'info--stretch'])}>
|
||||
{general.backstageUrl && <QRCode value={general.backstageUrl} size={qrSize} level='L' className='qr' />}
|
||||
|
||||
+5
-6
@@ -5,18 +5,17 @@ import Schedule from './Schedule';
|
||||
import { ScheduleProvider } from './ScheduleContext';
|
||||
import ScheduleNav from './ScheduleNav';
|
||||
|
||||
interface ScheduleExportProps {
|
||||
interface BackstageScheduleProps {
|
||||
selectedId: MaybeString;
|
||||
isBackstage?: boolean;
|
||||
}
|
||||
|
||||
export default memo(ScheduleExport);
|
||||
function ScheduleExport(props: ScheduleExportProps) {
|
||||
const { selectedId, isBackstage } = props;
|
||||
export default memo(BackstageSchedule);
|
||||
function BackstageSchedule(props: BackstageScheduleProps) {
|
||||
const { selectedId } = props;
|
||||
return (
|
||||
<ScheduleProvider selectedEventId={selectedId} isBackstage>
|
||||
<ScheduleNav className='schedule-nav-container' />
|
||||
<Schedule isProduction={isBackstage} className='schedule-container' />
|
||||
<Schedule isProduction className='schedule-container' />
|
||||
</ScheduleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { memo } from 'react';
|
||||
import { MaybeString } from 'ontime-types';
|
||||
|
||||
import Schedule from './Schedule';
|
||||
import { ScheduleProvider } from './ScheduleContext';
|
||||
import ScheduleNav from './ScheduleNav';
|
||||
|
||||
interface PublicScheduleProps {
|
||||
selectedId: MaybeString;
|
||||
}
|
||||
|
||||
export default memo(PublicSchedule);
|
||||
function PublicSchedule(props: PublicScheduleProps) {
|
||||
const { selectedId } = props;
|
||||
return (
|
||||
<ScheduleProvider selectedEventId={selectedId}>
|
||||
<ScheduleNav className='schedule-nav-container' />
|
||||
<Schedule className='schedule-container' />
|
||||
</ScheduleProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
$circle-size: clamp(8px, 0.75vw, 12px);
|
||||
$indeterminate-width: clamp(32px, 3vw, 48px);
|
||||
|
||||
.schedule {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -20,9 +17,9 @@ $indeterminate-width: clamp(32px, 3vw, 48px);
|
||||
|
||||
.entry-colour {
|
||||
background-color: var(--card-background-color-override, $viewer-card-bg-color);
|
||||
height: $circle-size;
|
||||
width: $circle-size;
|
||||
border-radius: $component-border-radius-full;
|
||||
height: clamp(8px, 0.75vw, 12px);
|
||||
width: clamp(8px, 0.75vw, 12px);
|
||||
border-radius: 6px;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -60,6 +57,7 @@ $indeterminate-width: clamp(32px, 3vw, 48px);
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: clamp(16px, 1.5vw, 24px);
|
||||
font-size: $base-font-size;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
@@ -72,10 +70,10 @@ $indeterminate-width: clamp(32px, 3vw, 48px);
|
||||
background-color: var(--color-override, $viewer-color);
|
||||
opacity: 0.2;
|
||||
|
||||
height: $circle-size;
|
||||
width: $circle-size;
|
||||
border-radius: $component-border-radius-full;
|
||||
margin-right: 0.5em;
|
||||
height: clamp(8px, 0.75vw, 12px);
|
||||
width: clamp(8px, 0.75vw, 12px);
|
||||
border-radius: 6px;
|
||||
margin-right: 8px;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: 1s;
|
||||
@@ -87,7 +85,7 @@ $indeterminate-width: clamp(32px, 3vw, 48px);
|
||||
}
|
||||
|
||||
&--indeterminate {
|
||||
width: $indeterminate-width;
|
||||
width: clamp(32px, 3vw, 48px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ export const ScheduleProvider = ({
|
||||
|
||||
const containerRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
// After the view is rendered, we paginate by hiding elements that dont fit
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Menu } from '@chakra-ui/react';
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import {
|
||||
@@ -22,8 +23,8 @@ import BlockRow from './cuesheet-table-elements/BlockRow';
|
||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||
import EventRow from './cuesheet-table-elements/EventRow';
|
||||
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||
import CuesheetTableMenu from './CuesheetTableMenu';
|
||||
import useColumnManager from './useColumnManager';
|
||||
|
||||
import style from './CuesheetTable.module.scss';
|
||||
@@ -92,13 +93,13 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const setAllVisible = useCallback(() => {
|
||||
const setAllVisible = () => {
|
||||
table.toggleAllColumnsVisible(true);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const resetColumnResizing = useCallback(() => {
|
||||
const resetColumnResizing = () => {
|
||||
setColumnSizing({});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const headerGroups = table.getHeaderGroups();
|
||||
const rowModel = table.getRowModel();
|
||||
@@ -164,24 +165,24 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<EventRow
|
||||
key={key}
|
||||
eventId={entry.id}
|
||||
eventIndex={eventIndex}
|
||||
rowIndex={index}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
skip={entry.skip}
|
||||
colour={entry.colour}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</EventRow>
|
||||
<Menu key={key} variant='ontime-on-dark' size='sm' isLazy>
|
||||
<EventRow
|
||||
eventIndex={eventIndex}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
skip={entry.skip}
|
||||
colour={entry.colour}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</EventRow>
|
||||
<CuesheetTableMenu event={entry} entryIndex={index} showModal={showModal} />
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +192,6 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CuesheetTableMenu showModal={showModal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+16
-21
@@ -5,30 +5,25 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { isOntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../../../common/hooks/useEventAction';
|
||||
import { cloneEvent } from '../../../../common/utils/eventsManager';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { cloneEvent } from '../../../common/utils/eventsManager';
|
||||
|
||||
interface CuesheetTableMenuActionsProps {
|
||||
eventId: string;
|
||||
interface CuesheetTableMenuProps {
|
||||
event: OntimeEvent;
|
||||
entryIndex: number;
|
||||
showModal: (entryId: string) => void;
|
||||
}
|
||||
|
||||
export default function CuesheetTableMenuActions(props: CuesheetTableMenuActionsProps) {
|
||||
const { eventId, entryIndex, showModal } = props;
|
||||
const { addEvent, getEventById, reorderEvent, deleteEvent } = useEventAction();
|
||||
export default function CuesheetTableMenu(props: CuesheetTableMenuProps) {
|
||||
const { event, entryIndex, showModal } = props;
|
||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||
|
||||
const handleCloneEvent = () => {
|
||||
const currentEvent = getEventById(eventId);
|
||||
if (!currentEvent || !isOntimeEvent(currentEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newEvent = cloneEvent(currentEvent);
|
||||
const newEvent = cloneEvent(event);
|
||||
try {
|
||||
addEvent(newEvent, { after: eventId });
|
||||
addEvent(newEvent, { after: event.id });
|
||||
} catch (_error) {
|
||||
// we do not handle errors here
|
||||
}
|
||||
@@ -36,14 +31,14 @@ export default function CuesheetTableMenuActions(props: CuesheetTableMenuActions
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
<MenuItem icon={<IoOptions />} onClick={() => showModal(eventId)}>
|
||||
<MenuItem icon={<IoOptions />} onClick={() => showModal(event.id)}>
|
||||
Edit ...
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { before: eventId })}>
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { before: event.id })}>
|
||||
Add event above
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { after: eventId })}>
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { after: event.id })}>
|
||||
Add event below
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoDuplicateOutline />} onClick={handleCloneEvent}>
|
||||
@@ -53,14 +48,14 @@ export default function CuesheetTableMenuActions(props: CuesheetTableMenuActions
|
||||
<MenuItem
|
||||
isDisabled={entryIndex < 1}
|
||||
icon={<IoArrowUp />}
|
||||
onClick={() => reorderEvent(eventId, entryIndex, entryIndex - 1)}
|
||||
onClick={() => reorderEvent(event.id, entryIndex, entryIndex - 1)}
|
||||
>
|
||||
Move up
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEvent(eventId, entryIndex, entryIndex + 1)}>
|
||||
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEvent(event.id, entryIndex, entryIndex + 1)}>
|
||||
Move down
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoTrash />} onClick={() => deleteEvent([eventId])}>
|
||||
<MenuItem icon={<IoTrash />} onClick={() => deleteEvent([event.id])}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
@@ -1,18 +1,15 @@
|
||||
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { IconButton, MenuButton } from '@chakra-ui/react';
|
||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||
import Color from 'color';
|
||||
|
||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface EventRowProps {
|
||||
eventId: string;
|
||||
eventIndex: number;
|
||||
rowIndex: number;
|
||||
isPast?: boolean;
|
||||
selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
|
||||
skip?: boolean;
|
||||
@@ -20,13 +17,11 @@ interface EventRowProps {
|
||||
}
|
||||
|
||||
function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
const { children, eventId, eventIndex, rowIndex, isPast, selectedRef, skip, colour } = props;
|
||||
const { children, eventIndex, isPast, selectedRef, skip, colour } = props;
|
||||
const { hideIndexColumn, showActionMenu } = useCuesheetOptions();
|
||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const { openMenu } = useCuesheetTableMenu();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
@@ -65,16 +60,12 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
>
|
||||
{showActionMenu && (
|
||||
<td className={style.actionColumn}>
|
||||
<IconButton
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size='sm'
|
||||
aria-label='Options'
|
||||
icon={<IoEllipsisHorizontal />}
|
||||
variant='ontime-subtle'
|
||||
onClick={(event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const yPos = 8 + rect.y + rect.height / 2;
|
||||
openMenu({ x: rect.x, y: yPos }, eventId, rowIndex);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { Menu, MenuButton, Portal } from '@chakra-ui/react';
|
||||
|
||||
import CuesheetTableMenuActionsProps from './CuesheetTableMenuActions';
|
||||
import { useCuesheetTableMenu } from './useCuesheetTableMenu';
|
||||
|
||||
interface CuesheetTableMenuProps {
|
||||
showModal: (eventId: string) => void;
|
||||
}
|
||||
|
||||
export default memo(CuesheetTableMenu);
|
||||
|
||||
function CuesheetTableMenu(props: CuesheetTableMenuProps) {
|
||||
const { showModal } = props;
|
||||
const { isOpen, eventId, entryIndex, position, closeMenu } = useCuesheetTableMenu();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
{isOpen && (
|
||||
<Menu isOpen size='sm' onClose={closeMenu} isLazy variant='ontime-on-dark'>
|
||||
<MenuButton
|
||||
position='absolute'
|
||||
left={position.x}
|
||||
top={position.y}
|
||||
pointerEvents='none'
|
||||
aria-hidden
|
||||
w={1}
|
||||
h={1}
|
||||
/>
|
||||
<CuesheetTableMenuActionsProps eventId={eventId} entryIndex={entryIndex} showModal={showModal} />
|
||||
</Menu>
|
||||
)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type Anchor = { x: number; y: number };
|
||||
|
||||
type OpenMenu = {
|
||||
isOpen: true;
|
||||
eventId: string;
|
||||
entryIndex: number;
|
||||
};
|
||||
|
||||
type ClosedMenu = {
|
||||
isOpen: false;
|
||||
eventId: null;
|
||||
entryIndex: null;
|
||||
};
|
||||
|
||||
type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & {
|
||||
position: Anchor;
|
||||
openMenu: (position: Anchor, eventId: string, entryIndex: number) => void;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
export const useCuesheetTableMenu = create<CuesheetTableMenuStore>((set) => ({
|
||||
isOpen: false,
|
||||
eventId: null,
|
||||
entryIndex: null,
|
||||
position: { x: 0, y: 0 },
|
||||
openMenu: (position: Anchor, eventId: string, entryIndex: number) =>
|
||||
set({ isOpen: true, position, eventId, entryIndex }),
|
||||
closeMenu: () => set({ isOpen: false }),
|
||||
}));
|
||||
@@ -12,7 +12,6 @@
|
||||
color: var(--color-override, $viewer-color);
|
||||
gap: $view-element-gap;
|
||||
padding: $view-outer-padding;
|
||||
font-size: $base-font-size;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 40vw;
|
||||
@@ -28,7 +27,6 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 25vh;
|
||||
color: var(--label-color-override, $viewer-label-color);
|
||||
}
|
||||
|
||||
/* =================== HEADER + EXTRAS ===================*/
|
||||
@@ -91,7 +89,7 @@
|
||||
row-gap: 0.5em;
|
||||
}
|
||||
|
||||
.time-entry {
|
||||
.aux-timers {
|
||||
&__label {
|
||||
font-size: $timer-label-size;
|
||||
color: var(--label-color-override, $viewer-label-color);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import { getIsPendingStart } from '../backstage/backstage.utils';
|
||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||
import PublicSchedule from '../common/schedule/PublicSchedule';
|
||||
|
||||
import { getPublicOptions, usePublicOptions } from './public.options';
|
||||
import { getCardData, getFirstStartTime } from './public.utils';
|
||||
@@ -52,7 +52,6 @@ export default function Public(props: BackstageProps) {
|
||||
useWindowTitle('Public Schedule');
|
||||
|
||||
// gather card data
|
||||
const hasEvents = events.length > 0;
|
||||
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
|
||||
publicEventNow,
|
||||
publicEventNext,
|
||||
@@ -64,11 +63,11 @@ export default function Public(props: BackstageProps) {
|
||||
// gather timer data
|
||||
const clock = formatTime(time.clock);
|
||||
const isPendingStart = getIsPendingStart(time.playback, time.phase);
|
||||
const scheduledStart = hasEvents && showNow ? '' : getFirstStartTime(events[0]);
|
||||
const scheduledStart = showNow ? '' : getFirstStartTime(events[0]);
|
||||
|
||||
// gather presentation styles
|
||||
const qrSize = Math.max(window.innerWidth / 15, 72);
|
||||
const showSchedule = hasEvents && screenHeight > 700; // in vertical screens we may not have space
|
||||
const showSchedule = screenHeight > 700; // in vertical screens we may not have space
|
||||
|
||||
// gather option data
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
@@ -86,31 +85,31 @@ export default function Public(props: BackstageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasEvents && <Empty text={getLocalizedString('countdown.waiting')} className='empty-container' />}
|
||||
{events.length === 0 && (
|
||||
<div className='empty-container'>
|
||||
<Empty text={getLocalizedString('countdown.waiting')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='card-container'>
|
||||
{showNow && hasEvents && (
|
||||
<TitleCard className='event now' label='now' title={nowMain} secondary={nowSecondary} />
|
||||
)}
|
||||
{showNow && <TitleCard className='event now' label='now' title={nowMain} secondary={nowSecondary} />}
|
||||
{!showNow && scheduledStart && (
|
||||
<div className='event'>
|
||||
<div className='title-card__placeholder'>{getLocalizedString('countdown.waiting')}</div>
|
||||
<div className='timer-group'>
|
||||
<div className='time-entry'>
|
||||
<div className={cx(['time-entry__label', isPendingStart && 'time-entry--pending'])}>
|
||||
<div className='aux-timers'>
|
||||
<div className={cx(['aux-timers__label', isPendingStart && 'aux-timers--pending'])}>
|
||||
{getLocalizedString('common.scheduled_start')}
|
||||
</div>
|
||||
<SuperscriptTime time={formatTime(scheduledStart)} className='time-entry__value' />
|
||||
<SuperscriptTime time={formatTime(scheduledStart)} className='aux-timers__value' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showNext && hasEvents && (
|
||||
<TitleCard className='event next' label='next' title={nextMain} secondary={nextSecondary} />
|
||||
)}
|
||||
{showNext && <TitleCard className='event next' label='next' title={nextMain} secondary={nextSecondary} />}
|
||||
</div>
|
||||
|
||||
{showSchedule && <ScheduleExport selectedId={publicSelectedId} />}
|
||||
{showSchedule && <PublicSchedule selectedId={publicSelectedId} />}
|
||||
|
||||
<div className={cx(['info', !showSchedule && 'info--stretch'])}>
|
||||
{general.publicUrl && <QRCode value={general.publicUrl} size={qrSize} level='L' className='qr' />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-electron",
|
||||
"version": "3.12.0",
|
||||
"version": "3.11.1-beta.1",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -250,29 +250,21 @@ function makeSettingsMenu(redirectWindow) {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Automation',
|
||||
label: 'Integrations',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Automation settings',
|
||||
click: () => redirectWindow('/editor?settings=automation__settings'),
|
||||
label: 'OSC settings',
|
||||
click: () => redirectWindow('/editor?settings=integrations__osc'),
|
||||
},
|
||||
{
|
||||
label: 'Manage automations',
|
||||
click: () => redirectWindow('/editor?settings=automation__automations'),
|
||||
},
|
||||
{
|
||||
label: 'Manage triggers',
|
||||
click: () => redirectWindow('/editor?settings=automation__triggers'),
|
||||
label: 'HTTP settings',
|
||||
click: () => redirectWindow('/editor?settings=integrations__http'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Network',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Share link',
|
||||
click: () => redirectWindow('/editor?settings=network__link'),
|
||||
},
|
||||
{
|
||||
label: 'Event log',
|
||||
click: () => redirectWindow('/editor?settings=network__log'),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "3.12.0",
|
||||
"version": "3.11.1-beta.1",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import { fromBuffer, type OscPacketOutput } from 'osc-min';
|
||||
import { fromBuffer } from 'osc-min';
|
||||
import * as dgram from 'node:dgram';
|
||||
|
||||
import type { IAdapter } from './IAdapter.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { integrationPayloadFromPath } from './utils/parse.js';
|
||||
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||
import { isOntimeCloud } from '../externals.js';
|
||||
|
||||
import { integrationPayloadFromPath } from './utils/parse.js';
|
||||
import type { IAdapter } from './IAdapter.js';
|
||||
|
||||
class OscServer implements IAdapter {
|
||||
private udpSocket: dgram.Socket | null = null;
|
||||
|
||||
@@ -28,16 +27,10 @@ class OscServer implements IAdapter {
|
||||
// params: used to create a nested object to patch with
|
||||
// args: extra data, only used on some API entries
|
||||
|
||||
let msg: OscPacketOutput;
|
||||
try {
|
||||
msg = fromBuffer(buf);
|
||||
} catch (_e) {
|
||||
logger.error(LogOrigin.Rx, 'OSC IN: Received invalid OSC message');
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = fromBuffer(buf);
|
||||
if (msg.oscType === 'bundle') {
|
||||
logger.error(LogOrigin.Rx, 'OSC IN: Ontime is unable to handle OSC bundles');
|
||||
//TODO: manage bundles
|
||||
logger.error(LogOrigin.Rx, `OSC IN: We don't take bundles`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +38,7 @@ class OscServer implements IAdapter {
|
||||
|
||||
// split message
|
||||
const [, ontimeKey, command, ...params] = address.split('/');
|
||||
const args = oscArgs[0]?.value ?? undefined;
|
||||
const args = oscArgs[0]?.value ?? undefined; //TODO: manage multiple args or mayeb we have no usecase
|
||||
|
||||
// get first part (ontime)
|
||||
if (ontimeKey !== 'ontime') {
|
||||
@@ -74,7 +67,7 @@ class OscServer implements IAdapter {
|
||||
this.udpSocket.bind(port);
|
||||
}
|
||||
shutdown() {
|
||||
logger.info(LogOrigin.Rx, 'OSC: Closing server');
|
||||
logger.info(LogOrigin.Rx, `OSC: Closing server`);
|
||||
this.udpSocket?.close();
|
||||
this.udpSocket = null;
|
||||
}
|
||||
|
||||
@@ -40,43 +40,43 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
describe('addTrigger()', () => {
|
||||
beforeEach(async () => {
|
||||
await deleteAllTriggers();
|
||||
beforeEach(() => {
|
||||
deleteAllTriggers();
|
||||
});
|
||||
|
||||
it('should accept a valid trigger', async () => {
|
||||
it('should accept a valid automation', () => {
|
||||
const testData: TriggerDTO = {
|
||||
title: 'test',
|
||||
trigger: TimerLifeCycle.onLoad,
|
||||
automationId: 'test-automation-id',
|
||||
};
|
||||
|
||||
const trigger = await addTrigger(testData);
|
||||
const trigger = addTrigger(testData);
|
||||
expect(trigger).toMatchObject(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editTrigger()', () => {
|
||||
beforeEach(async () => {
|
||||
await deleteAllTriggers();
|
||||
await addTrigger({
|
||||
beforeEach(() => {
|
||||
deleteAllTriggers();
|
||||
addTrigger({
|
||||
title: 'test-osc',
|
||||
trigger: TimerLifeCycle.onLoad,
|
||||
automationId: 'test-osc-automation',
|
||||
});
|
||||
await addTrigger({
|
||||
addTrigger({
|
||||
title: 'test-http',
|
||||
trigger: TimerLifeCycle.onFinish,
|
||||
automationId: 'test-http-automation',
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit the contents of a trigger', async () => {
|
||||
it('should edit the contents of an automation', () => {
|
||||
const triggers = getAutomationTriggers();
|
||||
const fistTrigger = triggers[0];
|
||||
expect(fistTrigger).toMatchObject({ id: expect.any(String), title: 'test-osc' });
|
||||
|
||||
const editedOSC = await editTrigger(fistTrigger.id, {
|
||||
const editedOSC = editTrigger(fistTrigger.id, {
|
||||
title: 'edited-title',
|
||||
trigger: TimerLifeCycle.onDanger,
|
||||
automationId: 'test-osc-automation',
|
||||
@@ -92,9 +92,9 @@ describe('editTrigger()', () => {
|
||||
});
|
||||
|
||||
describe('deleteTrigger()', () => {
|
||||
beforeEach(async () => {
|
||||
await deleteAllTriggers();
|
||||
await addTrigger({
|
||||
beforeEach(() => {
|
||||
deleteAllTriggers();
|
||||
addTrigger({
|
||||
title: 'test-osc',
|
||||
trigger: TimerLifeCycle.onLoad,
|
||||
automationId: 'test-osc-automation',
|
||||
@@ -106,13 +106,13 @@ describe('deleteTrigger()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove an automation from the list', async () => {
|
||||
it('should remove an automation from the list', () => {
|
||||
const triggers = getAutomationTriggers();
|
||||
expect(triggers.length).toEqual(2);
|
||||
const fistTrigger = triggers[0];
|
||||
expect(fistTrigger).toMatchObject({ id: expect.any(String), title: 'test-osc' });
|
||||
|
||||
await deleteTrigger(fistTrigger.id);
|
||||
deleteTrigger(fistTrigger.id);
|
||||
const removed = getAutomationTriggers();
|
||||
expect(removed.length).toEqual(1);
|
||||
expect(removed[0].title).not.toEqual('test-osc');
|
||||
@@ -120,11 +120,11 @@ describe('deleteTrigger()', () => {
|
||||
});
|
||||
|
||||
describe('addAutomation()', () => {
|
||||
beforeEach(async () => {
|
||||
await deleteAll();
|
||||
beforeEach(() => {
|
||||
deleteAll();
|
||||
});
|
||||
|
||||
it('should accept a valid automation', async () => {
|
||||
it('should accept a valid automation', () => {
|
||||
const testData: AutomationDTO = {
|
||||
title: 'test',
|
||||
filterRule: 'all',
|
||||
@@ -132,24 +132,24 @@ describe('addAutomation()', () => {
|
||||
outputs: [makeOSCAction(), makeHTTPAction()],
|
||||
};
|
||||
|
||||
const automation = await addAutomation(testData);
|
||||
const automation = addAutomation(testData);
|
||||
const automations = getAutomations();
|
||||
expect(automations[automation.id]).toMatchObject(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editAutomation()', async () => {
|
||||
describe('editAutomation()', () => {
|
||||
// saving the ID of the added automation
|
||||
let firstAutomation: Automation;
|
||||
beforeEach(async () => {
|
||||
await deleteAll();
|
||||
firstAutomation = await addAutomation({
|
||||
beforeEach(() => {
|
||||
deleteAll();
|
||||
firstAutomation = addAutomation({
|
||||
title: 'test-osc',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
outputs: [],
|
||||
});
|
||||
await addAutomation({
|
||||
addAutomation({
|
||||
title: 'test-http',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
@@ -157,7 +157,7 @@ describe('editAutomation()', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit the contents of an automation', async () => {
|
||||
it('should edit the contents of an automation', () => {
|
||||
const automations = getAutomations();
|
||||
expect(Object.keys(automations).length).toEqual(2);
|
||||
expect(automations[firstAutomation.id]).toMatchObject({
|
||||
@@ -168,7 +168,7 @@ describe('editAutomation()', async () => {
|
||||
outputs: expect.any(Array),
|
||||
});
|
||||
|
||||
const editedOSC = await editAutomation(firstAutomation.id, {
|
||||
const editedOSC = editAutomation(firstAutomation.id, {
|
||||
title: 'edited-title',
|
||||
filterRule: 'any',
|
||||
filters: [],
|
||||
@@ -188,9 +188,9 @@ describe('editAutomation()', async () => {
|
||||
describe('deleteAutomation()', () => {
|
||||
// saving the ID of the added automation
|
||||
let firstAutomation: Automation;
|
||||
beforeEach(async () => {
|
||||
await deleteAll();
|
||||
firstAutomation = await addAutomation({
|
||||
beforeEach(() => {
|
||||
deleteAll();
|
||||
firstAutomation = addAutomation({
|
||||
title: 'test-osc',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
@@ -198,18 +198,18 @@ describe('deleteAutomation()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove m automation from the list', async () => {
|
||||
it('should remove m automation from the list', () => {
|
||||
const automations = getAutomations();
|
||||
expect(Object.keys(automations).length).toEqual(1);
|
||||
|
||||
await deleteAutomation(Object.keys(automations)[0]);
|
||||
deleteAutomation(Object.keys(automations)[0]);
|
||||
const removed = getAutomations();
|
||||
expect(Object.keys(removed).length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not remove an automation which is in use', async () => {
|
||||
it('should not remove an automation which is in use', () => {
|
||||
const automations = getAutomations();
|
||||
await addTrigger({
|
||||
addTrigger({
|
||||
title: 'test-automation',
|
||||
trigger: TimerLifeCycle.onLoad,
|
||||
automationId: firstAutomation.id,
|
||||
@@ -227,6 +227,6 @@ describe('deleteAutomation()', () => {
|
||||
outputs: expect.any(Array),
|
||||
});
|
||||
|
||||
await expect(deleteAutomation(automationId)).rejects.toThrowError();
|
||||
expect(() => deleteAutomation(automationId)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,29 +39,29 @@ describe('triggerAction()', () => {
|
||||
let oscSpy = vi.spyOn(oscClient, 'emitOSC');
|
||||
let httpSpy = vi.spyOn(httpClient, 'emitHTTP');
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
oscSpy = vi.spyOn(oscClient, 'emitOSC').mockImplementation(() => {});
|
||||
httpSpy = vi.spyOn(httpClient, 'emitHTTP').mockImplementation(() => {});
|
||||
|
||||
await deleteAllTriggers();
|
||||
const oscAutomation = await addAutomation({
|
||||
deleteAllTriggers();
|
||||
const oscAutomation = addAutomation({
|
||||
title: 'test-osc',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
outputs: [makeOSCAction()],
|
||||
});
|
||||
const httpAutomation = await addAutomation({
|
||||
const httpAutomation = addAutomation({
|
||||
title: 'test-http',
|
||||
filterRule: 'any',
|
||||
filters: [],
|
||||
outputs: [makeHTTPAction()],
|
||||
});
|
||||
await addTrigger({
|
||||
addTrigger({
|
||||
title: 'test-osc',
|
||||
trigger: TimerLifeCycle.onLoad,
|
||||
automationId: oscAutomation.id,
|
||||
});
|
||||
await addTrigger({
|
||||
addTrigger({
|
||||
title: 'test-http',
|
||||
trigger: TimerLifeCycle.onFinish,
|
||||
automationId: httpAutomation.id,
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { parseOutput } from '../automation.validation.js';
|
||||
|
||||
describe('parseOutput', () => {
|
||||
describe('handles OSC outputs', () => {
|
||||
it('parses a valid payload', () => {
|
||||
const payload = {
|
||||
type: 'osc',
|
||||
targetIP: 'localhost',
|
||||
targetPort: 1234,
|
||||
address: '/test',
|
||||
args: 'test',
|
||||
};
|
||||
const result = parseOutput(payload);
|
||||
expect(result).toStrictEqual(payload);
|
||||
});
|
||||
|
||||
it('throws on a invalid payload', () => {
|
||||
const payload = {
|
||||
type: 'osc',
|
||||
targetIP: 1234,
|
||||
targetPort: 1234,
|
||||
address: '/test',
|
||||
args: 'test',
|
||||
};
|
||||
expect(() => parseOutput(payload)).toThrow();
|
||||
});
|
||||
});
|
||||
describe('handles HTTP outputs', () => {
|
||||
it('parses a valid payload', () => {
|
||||
const payload = {
|
||||
type: 'http',
|
||||
url: 'http://asdasdas',
|
||||
};
|
||||
const result = parseOutput(payload);
|
||||
expect(result).toStrictEqual(payload);
|
||||
});
|
||||
|
||||
it('throws on a invalid payload', () => {
|
||||
const payload = {
|
||||
type: 'http',
|
||||
};
|
||||
expect(() => parseOutput(payload)).toThrow();
|
||||
});
|
||||
});
|
||||
describe('handles Ontime outputs', () => {
|
||||
it('parses a valid payload', () => {
|
||||
const auxStart = {
|
||||
type: 'ontime',
|
||||
action: 'aux-start',
|
||||
};
|
||||
expect(parseOutput(auxStart)).toStrictEqual(auxStart);
|
||||
const auxStop = {
|
||||
type: 'ontime',
|
||||
action: 'aux-stop',
|
||||
};
|
||||
expect(parseOutput(auxStop)).toStrictEqual(auxStop);
|
||||
const auxPause = {
|
||||
type: 'ontime',
|
||||
action: 'aux-pause',
|
||||
};
|
||||
expect(parseOutput(auxPause)).toStrictEqual(auxPause);
|
||||
});
|
||||
|
||||
it('removes extra properties', () => {
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'aux-start',
|
||||
time: 10,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'ontime',
|
||||
action: 'aux-start',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on a invalid payload', () => {
|
||||
const payload = {
|
||||
type: 'ontime',
|
||||
action: 'not-exist',
|
||||
};
|
||||
expect(() => parseOutput(payload)).toThrow();
|
||||
});
|
||||
|
||||
it('parses message-set', () => {
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-set',
|
||||
text: 'test',
|
||||
visible: 'true',
|
||||
}),
|
||||
).toMatchObject({
|
||||
text: 'test',
|
||||
visible: true,
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-set',
|
||||
text: '',
|
||||
visible: 'false',
|
||||
}),
|
||||
).toMatchObject({
|
||||
text: undefined,
|
||||
visible: false,
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-set',
|
||||
text: '',
|
||||
visible: '',
|
||||
}),
|
||||
).toMatchObject({
|
||||
text: undefined,
|
||||
visible: undefined,
|
||||
});
|
||||
expect(() =>
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-set',
|
||||
text: 123,
|
||||
visible: '',
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('parses message-secondary', () => {});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: 'test',
|
||||
}),
|
||||
).toMatchObject({
|
||||
secondarySource: null,
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: '',
|
||||
}),
|
||||
).toMatchObject({
|
||||
secondarySource: null,
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: 'aux',
|
||||
}),
|
||||
).toMatchObject({
|
||||
secondarySource: 'aux',
|
||||
});
|
||||
expect(
|
||||
parseOutput({
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: 'external',
|
||||
}),
|
||||
).toMatchObject({
|
||||
secondarySource: 'external',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,20 @@
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
import { Automation, AutomationSettings, ErrorResponse, Trigger } from 'ontime-types';
|
||||
import { Automation, AutomationOutput, AutomationSettings, ErrorResponse, Trigger } from 'ontime-types';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import { oscServer } from '../../adapters/OscAdapter.js';
|
||||
|
||||
import * as automationDao from './automation.dao.js';
|
||||
import * as automationService from './automation.service.js';
|
||||
import { parseOutput } from './automation.validation.js';
|
||||
import { oscServer } from '../../adapters/OscAdapter.js';
|
||||
|
||||
export function getAutomationSettings(_req: Request, res: Response<AutomationSettings>) {
|
||||
res.json(automationDao.getAutomationSettings());
|
||||
}
|
||||
|
||||
export async function postAutomationSettings(req: Request, res: Response<AutomationSettings | ErrorResponse>) {
|
||||
export function postAutomationSettings(req: Request, res: Response<AutomationSettings | ErrorResponse>) {
|
||||
try {
|
||||
// body payload is a patch object that must contain root properties
|
||||
const automationSettings = await automationDao.editAutomationSettings({
|
||||
const automationSettings = automationDao.editAutomationSettings({
|
||||
enabledAutomations: req.body.enabledAutomations,
|
||||
enabledOscIn: req.body.enabledOscIn,
|
||||
oscPortIn: req.body.oscPortIn,
|
||||
@@ -35,9 +33,9 @@ export async function postAutomationSettings(req: Request, res: Response<Automat
|
||||
}
|
||||
}
|
||||
|
||||
export async function postTrigger(req: Request, res: Response<Trigger | ErrorResponse>) {
|
||||
export function postTrigger(req: Request, res: Response<Trigger | ErrorResponse>) {
|
||||
try {
|
||||
const automation = await automationDao.addTrigger({
|
||||
const automation = automationDao.addTrigger({
|
||||
title: req.body.title,
|
||||
trigger: req.body.trigger,
|
||||
automationId: req.body.automationId,
|
||||
@@ -49,10 +47,10 @@ export async function postTrigger(req: Request, res: Response<Trigger | ErrorRes
|
||||
}
|
||||
}
|
||||
|
||||
export async function putTrigger(req: Request, res: Response<Trigger | ErrorResponse>) {
|
||||
export function putTrigger(req: Request, res: Response<Trigger | ErrorResponse>) {
|
||||
try {
|
||||
// body payload is a patch object
|
||||
const automation = await automationDao.editTrigger(req.params.id, {
|
||||
const automation = automationDao.editTrigger(req.params.id, {
|
||||
title: req.body.title ?? undefined,
|
||||
trigger: req.body.trigger ?? undefined,
|
||||
automationId: req.body.automationId ?? undefined,
|
||||
@@ -64,9 +62,9 @@ export async function putTrigger(req: Request, res: Response<Trigger | ErrorResp
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTrigger(req: Request, res: Response<void | ErrorResponse>) {
|
||||
export function deleteTrigger(req: Request, res: Response<void | ErrorResponse>) {
|
||||
try {
|
||||
await automationDao.deleteTrigger(req.params.id);
|
||||
automationDao.deleteTrigger(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -74,9 +72,9 @@ export async function deleteTrigger(req: Request, res: Response<void | ErrorResp
|
||||
}
|
||||
}
|
||||
|
||||
export async function postAutomation(req: Request, res: Response<Automation | ErrorResponse>) {
|
||||
export function postAutomation(req: Request, res: Response<Automation | ErrorResponse>) {
|
||||
try {
|
||||
const newAutomation = await automationDao.addAutomation({
|
||||
const newAutomation = automationDao.addAutomation({
|
||||
title: req.body.title,
|
||||
filterRule: req.body.filterRule,
|
||||
filters: req.body.filters,
|
||||
@@ -89,9 +87,9 @@ export async function postAutomation(req: Request, res: Response<Automation | Er
|
||||
}
|
||||
}
|
||||
|
||||
export async function editAutomation(req: Request, res: Response<Automation | ErrorResponse>) {
|
||||
export function editAutomation(req: Request, res: Response<Automation | ErrorResponse>) {
|
||||
try {
|
||||
const newAutomation = await automationDao.editAutomation(req.params.id, {
|
||||
const newAutomation = automationDao.editAutomation(req.params.id, {
|
||||
title: req.body.title,
|
||||
filterRule: req.body.filterRule,
|
||||
filters: req.body.filters,
|
||||
@@ -104,9 +102,9 @@ export async function editAutomation(req: Request, res: Response<Automation | Er
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAutomation(req: Request, res: Response<void | ErrorResponse>) {
|
||||
export function deleteAutomation(req: Request, res: Response<void | ErrorResponse>) {
|
||||
try {
|
||||
await automationDao.deleteAutomation(req.params.id);
|
||||
automationDao.deleteAutomation(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -116,9 +114,8 @@ export async function deleteAutomation(req: Request, res: Response<void | ErrorR
|
||||
|
||||
export function testOutput(req: Request, res: Response<void | ErrorResponse>) {
|
||||
try {
|
||||
const payload = req.body;
|
||||
const parsed = parseOutput(payload);
|
||||
automationService.testOutput(parsed);
|
||||
const payload = req.body as AutomationOutput;
|
||||
automationService.testOutput(payload);
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import type {
|
||||
Automation,
|
||||
AutomationDTO,
|
||||
AutomationSettings,
|
||||
NormalisedAutomation,
|
||||
Trigger,
|
||||
TriggerDTO,
|
||||
} from 'ontime-types';
|
||||
import type { Automation, AutomationDTO, AutomationSettings, NormalisedAutomation, Trigger, TriggerDTO } from 'ontime-types';
|
||||
import { deleteAtIndex, generateId } from 'ontime-utils';
|
||||
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
@@ -41,27 +34,27 @@ export function getAutomations(): NormalisedAutomation {
|
||||
/**
|
||||
* Patches the automation settings object
|
||||
*/
|
||||
export async function editAutomationSettings(settings: Partial<AutomationSettings>): Promise<AutomationSettings> {
|
||||
await saveChanges(settings);
|
||||
export function editAutomationSettings(settings: Partial<AutomationSettings>): AutomationSettings {
|
||||
saveChanges(settings);
|
||||
return getAutomationSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a validated automation to the store
|
||||
*/
|
||||
export async function addTrigger(newTrigger: TriggerDTO): Promise<Trigger> {
|
||||
export function addTrigger(newTrigger: TriggerDTO): Trigger {
|
||||
const triggers = getAutomationTriggers();
|
||||
const id = getUniqueTriggerId(triggers);
|
||||
const trigger = { ...newTrigger, id };
|
||||
triggers.push(trigger);
|
||||
await saveChanges({ triggers });
|
||||
saveChanges({ triggers });
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches an existing automation trigger
|
||||
*/
|
||||
export async function editTrigger(id: string, newTrigger: TriggerDTO): Promise<Trigger> {
|
||||
export function editTrigger(id: string, newTrigger: TriggerDTO): Trigger {
|
||||
const triggers = getAutomationTriggers();
|
||||
const index = triggers.findIndex((trigger) => trigger.id === id);
|
||||
|
||||
@@ -70,14 +63,14 @@ export async function editTrigger(id: string, newTrigger: TriggerDTO): Promise<T
|
||||
}
|
||||
|
||||
triggers[index] = { ...triggers[index], ...newTrigger };
|
||||
await saveChanges({ triggers });
|
||||
saveChanges({ triggers });
|
||||
return triggers[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an automation trigger given its ID
|
||||
*/
|
||||
export async function deleteTrigger(id: string): Promise<void> {
|
||||
export function deleteTrigger(id: string): void {
|
||||
let triggers = getAutomationTriggers();
|
||||
const index = triggers.findIndex((trigger) => trigger.id === id);
|
||||
|
||||
@@ -86,53 +79,53 @@ export async function deleteTrigger(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
triggers = deleteAtIndex(index, triggers);
|
||||
await saveChanges({ triggers });
|
||||
saveChanges({ triggers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all project automation triggers
|
||||
*/
|
||||
export async function deleteAllTriggers(): Promise<void> {
|
||||
await saveChanges({ triggers: [] });
|
||||
export function deleteAllTriggers(): void {
|
||||
saveChanges({ triggers: [] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all project automation triggers and automations
|
||||
* We do this together to avoid issues with missing references
|
||||
*/
|
||||
export async function deleteAll() {
|
||||
await saveChanges({ triggers: [], automations: {} });
|
||||
export function deleteAll(): void {
|
||||
saveChanges({ triggers: [], automations: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a validated automation to the store
|
||||
*/
|
||||
export async function addAutomation(newAutomation: AutomationDTO): Promise<Automation> {
|
||||
export function addAutomation(newAutomation: AutomationDTO): Automation {
|
||||
const automations = getAutomations();
|
||||
const id = getUniqueAutomationId(automations);
|
||||
automations[id] = { ...newAutomation, id };
|
||||
await saveChanges({ automations });
|
||||
saveChanges({ automations });
|
||||
return automations[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing automation with a new entry
|
||||
*/
|
||||
export async function editAutomation(id: string, newAutomation: AutomationDTO): Promise<Automation> {
|
||||
export function editAutomation(id: string, newAutomation: AutomationDTO): Automation {
|
||||
const automations = getAutomations();
|
||||
if (!Object.hasOwn(automations, id)) {
|
||||
throw new Error(`Automation with id ${id} not found`);
|
||||
}
|
||||
|
||||
automations[id] = { ...newAutomation, id };
|
||||
await saveChanges({ automations });
|
||||
saveChanges({ automations });
|
||||
return automations[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a automation given its ID
|
||||
*/
|
||||
export async function deleteAutomation(id: string): Promise<void> {
|
||||
export function deleteAutomation(id: string): void {
|
||||
const automations = getAutomations();
|
||||
// ignore request if automation does not exist
|
||||
if (!Object.hasOwn(automations, id)) {
|
||||
@@ -147,7 +140,7 @@ export async function deleteAutomation(id: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
delete automations[id];
|
||||
await saveChanges({ automations });
|
||||
saveChanges({ automations });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
isHTTPOutput,
|
||||
isOntimeAction,
|
||||
isOSCOutput,
|
||||
LogOrigin,
|
||||
type AutomationFilter,
|
||||
type AutomationOutput,
|
||||
type FilterRule,
|
||||
@@ -10,7 +8,6 @@ import {
|
||||
} from 'ontime-types';
|
||||
import { getPropertyFromPath } from 'ontime-utils';
|
||||
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { getState, type RuntimeState } from '../../stores/runtimeState.js';
|
||||
import { isOntimeCloud } from '../../externals.js';
|
||||
|
||||
@@ -18,7 +15,6 @@ import { emitOSC } from './clients/osc.client.js';
|
||||
import { emitHTTP } from './clients/http.client.js';
|
||||
import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js';
|
||||
import { isBooleanEquals, isGreaterThan, isLessThan } from './automation.utils.js';
|
||||
import { toOntimeAction } from './clients/ontime.client.js';
|
||||
|
||||
/**
|
||||
* Exposes a method for triggering actions based on a TimerLifeCycle event
|
||||
@@ -41,7 +37,7 @@ export function triggerAutomations(event: TimerLifeCycle, state: RuntimeState) {
|
||||
|
||||
triggerAutomations.forEach((trigger) => {
|
||||
const automation = automations[trigger.automationId];
|
||||
if (!automation || automation.outputs.length === 0) {
|
||||
if (!automation) {
|
||||
return;
|
||||
}
|
||||
const shouldSend = testConditions(automation.filters, automation.filterRule, state);
|
||||
@@ -121,14 +117,12 @@ export function testConditions(
|
||||
function send(output: AutomationOutput[], state?: RuntimeState) {
|
||||
const stateSnapshot = state ?? getState();
|
||||
output.forEach((payload) => {
|
||||
if (isOSCOutput(payload) && !isOntimeCloud) {
|
||||
emitOSC(payload, stateSnapshot);
|
||||
if (isOSCOutput(payload)) {
|
||||
if (!isOntimeCloud) {
|
||||
emitOSC(payload, stateSnapshot);
|
||||
}
|
||||
} else if (isHTTPOutput(payload)) {
|
||||
emitHTTP(payload, stateSnapshot);
|
||||
} else if (isOntimeAction(payload)) {
|
||||
toOntimeAction(payload);
|
||||
} else {
|
||||
logger.warning(LogOrigin.Tx, `Unknown output type: ${payload}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FilterRule, MaybeNumber, OntimeAction } from 'ontime-types';
|
||||
import { FilterRule, MaybeNumber } from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils';
|
||||
import type { OscArgOrArrayInput, OscArgInput } from 'osc-min';
|
||||
|
||||
@@ -12,10 +12,6 @@ export function isFilterRule(value: string): value is FilterRule {
|
||||
return value === 'all' || value === 'any';
|
||||
}
|
||||
|
||||
export function isOntimeActionAction(value: string): value is OntimeAction['action'] {
|
||||
return ['aux-start', 'aux-stop', 'aux-pause', 'aux-set', 'message-set', 'message-secondary'].includes(value);
|
||||
}
|
||||
|
||||
function toOscValue(argString: string): OscArgInput {
|
||||
const argAsNum = Number(argString);
|
||||
// NOTE: number like: 1 2.0 33333
|
||||
|
||||
@@ -3,19 +3,16 @@ import {
|
||||
AutomationFilter,
|
||||
AutomationOutput,
|
||||
HTTPOutput,
|
||||
OntimeAction,
|
||||
OSCOutput,
|
||||
SecondarySource,
|
||||
timerLifecycleValues,
|
||||
} from 'ontime-types';
|
||||
import { parseUserTime } from 'ontime-utils';
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { body, oneOf, param, validationResult } from 'express-validator';
|
||||
|
||||
import * as assert from '../../utils/assert.js';
|
||||
|
||||
import { isFilterOperator, isFilterRule, isOntimeActionAction } from './automation.utils.js';
|
||||
import { isFilterOperator, isFilterRule } from './automation.utils.js';
|
||||
|
||||
export const paramContainsId = [
|
||||
param('id').exists(),
|
||||
@@ -134,13 +131,43 @@ function validateFilters(filters: Array<unknown>): filters is AutomationFilter[]
|
||||
|
||||
function validateOutput(output: Array<unknown>): output is AutomationOutput[] {
|
||||
output.forEach((payload) => {
|
||||
parseOutput(payload);
|
||||
assert.isObject(payload);
|
||||
assert.hasKeys(payload, ['type']);
|
||||
const { type } = payload;
|
||||
assert.isString(type);
|
||||
|
||||
if (type === 'osc') {
|
||||
validateOSCOutput(payload);
|
||||
} else if (type === 'http') {
|
||||
validateHttpOutput(payload);
|
||||
} else {
|
||||
throw new Error('Invalid automation');
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateOSCOutput(payload: object): payload is OSCOutput {
|
||||
assert.hasKeys(payload, ['targetIP', 'targetPort', 'address', 'args']);
|
||||
const { targetIP, targetPort, address, args } = payload;
|
||||
assert.isString(targetIP);
|
||||
assert.isNumber(targetPort);
|
||||
assert.isString(address);
|
||||
if (typeof args !== 'string' && typeof args !== 'number') {
|
||||
throw new Error('Invalid automation');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateHttpOutput(payload: object): payload is HTTPOutput {
|
||||
assert.hasKeys(payload, ['url']);
|
||||
const { url } = payload;
|
||||
assert.isString(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const validateTestPayload = [
|
||||
body('type').exists().isIn(['osc', 'http', 'ontime']),
|
||||
body('type').exists().isIn(['osc', 'http']),
|
||||
|
||||
// validation for OSC message
|
||||
oneOf([
|
||||
@@ -155,143 +182,9 @@ export const validateTestPayload = [
|
||||
// validation for HTTP message
|
||||
body('url').if(body('type').equals('http')).isURL({ require_tld: false }).trim(),
|
||||
|
||||
// validation for Ontime actions
|
||||
body('action').if(body('type').equals('ontime')).isString().trim(),
|
||||
body('text').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||
body('time').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||
body('visible').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||
body('secondarySource').if(body('type').equals('ontime')).optional().isString().trim(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Sanitises an output object
|
||||
* @Throws if the output is invalid
|
||||
*/
|
||||
export function parseOutput(maybeOutput: unknown): AutomationOutput {
|
||||
assert.isObject(maybeOutput);
|
||||
assert.hasKeys(maybeOutput, ['type']);
|
||||
|
||||
const { type } = maybeOutput;
|
||||
assert.isString(type);
|
||||
|
||||
if (type === 'osc') {
|
||||
return parseOSCOutput(maybeOutput);
|
||||
} else if (type === 'http') {
|
||||
return parseHTTPOutput(maybeOutput);
|
||||
} else if (type === 'ontime') {
|
||||
return parseOntimeAction(maybeOutput);
|
||||
} else {
|
||||
throw new Error('Invalid automation output');
|
||||
}
|
||||
}
|
||||
|
||||
function parseOSCOutput(maybeOSCOutput: object): OSCOutput {
|
||||
assert.hasKeys(maybeOSCOutput, ['targetIP', 'targetPort', 'address', 'args']);
|
||||
assert.isString(maybeOSCOutput.targetIP);
|
||||
assert.isNumber(maybeOSCOutput.targetPort);
|
||||
assert.isString(maybeOSCOutput.address);
|
||||
assert.isString(maybeOSCOutput.args);
|
||||
|
||||
return {
|
||||
type: 'osc',
|
||||
targetIP: maybeOSCOutput.targetIP,
|
||||
targetPort: maybeOSCOutput.targetPort,
|
||||
address: maybeOSCOutput.address,
|
||||
args: maybeOSCOutput.args,
|
||||
};
|
||||
}
|
||||
|
||||
function parseHTTPOutput(maybeHTTPOutput: object): HTTPOutput {
|
||||
assert.hasKeys(maybeHTTPOutput, ['url']);
|
||||
assert.isString(maybeHTTPOutput.url);
|
||||
|
||||
return {
|
||||
type: 'http',
|
||||
url: maybeHTTPOutput.url,
|
||||
};
|
||||
}
|
||||
|
||||
function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
|
||||
assert.hasKeys(maybeOntimeAction, ['action']);
|
||||
assert.isString(maybeOntimeAction.action);
|
||||
|
||||
if (!isOntimeActionAction(maybeOntimeAction.action)) {
|
||||
throw new Error('Invalid Ontime action');
|
||||
}
|
||||
|
||||
// we know we have a valid action, deal with special cases
|
||||
|
||||
if (maybeOntimeAction.action === 'aux-set') {
|
||||
assert.hasKeys(maybeOntimeAction, ['time']);
|
||||
assert.isString(maybeOntimeAction.time);
|
||||
|
||||
return {
|
||||
type: 'ontime',
|
||||
action: 'aux-set',
|
||||
time: parseUserTime(maybeOntimeAction.time),
|
||||
};
|
||||
}
|
||||
|
||||
if (maybeOntimeAction.action === 'message-set') {
|
||||
assert.hasKeys(maybeOntimeAction, ['text', 'visible']);
|
||||
assert.isString(maybeOntimeAction.text);
|
||||
assert.isString(maybeOntimeAction.visible);
|
||||
|
||||
return {
|
||||
type: 'ontime',
|
||||
action: 'message-set',
|
||||
text: indeterminateText(maybeOntimeAction.text),
|
||||
visible: indeterminateBooleanString(maybeOntimeAction.visible),
|
||||
};
|
||||
}
|
||||
|
||||
if (maybeOntimeAction.action === 'message-secondary') {
|
||||
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
|
||||
assert.isString(maybeOntimeAction.secondarySource);
|
||||
|
||||
return {
|
||||
type: 'ontime',
|
||||
action: 'message-secondary',
|
||||
secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'ontime',
|
||||
action: maybeOntimeAction.action,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to parse a text which may be indeterminate
|
||||
* "some text" -> string
|
||||
* "" -> undefined
|
||||
*/
|
||||
function indeterminateText(value: string): string | undefined {
|
||||
return value === '' ? undefined : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to parse boolean values in transit
|
||||
* "true" -> true
|
||||
* "false" -> false
|
||||
* "" | "null" -> undefined
|
||||
*/
|
||||
function indeterminateBooleanString(value: string): boolean | undefined {
|
||||
return value === '' ? undefined : value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate the secondary source
|
||||
*/
|
||||
function chooseSecondarySource(value: string): SecondarySource {
|
||||
if (value === 'aux') return 'aux';
|
||||
if (value === 'external') return 'external';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { LogOrigin, OntimeAction } from 'ontime-types';
|
||||
|
||||
import { logger } from '../../../classes/Logger.js';
|
||||
import { auxTimerService } from '../../../services/aux-timer-service/AuxTimerService.js';
|
||||
import * as messageService from '../../../services/message-service/MessageService.js';
|
||||
|
||||
export function toOntimeAction(action: OntimeAction) {
|
||||
switch (action.action) {
|
||||
// Aux timer actions
|
||||
case 'aux-start':
|
||||
auxTimerService.start();
|
||||
break;
|
||||
case 'aux-stop':
|
||||
auxTimerService.stop();
|
||||
break;
|
||||
case 'aux-pause':
|
||||
auxTimerService.pause();
|
||||
break;
|
||||
case 'aux-set': {
|
||||
auxTimerService.setTime(action.time);
|
||||
break;
|
||||
}
|
||||
|
||||
// Message actions
|
||||
case 'message-set': {
|
||||
messageService.patch({
|
||||
timer: {
|
||||
text: action.text,
|
||||
visible: action.visible,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'message-secondary': {
|
||||
messageService.patch({
|
||||
timer: {
|
||||
secondarySource: action.secondarySource,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// @ts-expect-error -- this guard checks that we handled all the cases, but we still want to log just in case
|
||||
logger.warning(LogOrigin.Tx, `Unknown action type: ${action.type}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -133,8 +133,7 @@ export const validateFilenameParam = [
|
||||
.customSanitizer((input: string) => sanitize(input))
|
||||
.withMessage('Failed to sanitize the filename')
|
||||
.notEmpty()
|
||||
.withMessage('Filename was empty or contained only invalid characters')
|
||||
.customSanitizer((input: string) => ensureJsonExtension(input)),
|
||||
.withMessage('Filename was empty or contained only invalid characters'),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
@@ -144,7 +144,7 @@ export const initAssets = async () => {
|
||||
checkStart(OntimeStartOrder.InitAssets);
|
||||
await clearUploadfolder();
|
||||
populateStyles();
|
||||
await populateDemo();
|
||||
populateDemo();
|
||||
const project = await initialiseProject();
|
||||
logger.info(LogOrigin.Server, `Initialised Ontime with ${project}`);
|
||||
};
|
||||
@@ -200,7 +200,7 @@ export const startServer = async (
|
||||
// initialise rundown service
|
||||
const persistedRundown = getDataProvider().getRundown();
|
||||
const persistedCustomFields = getDataProvider().getCustomFields();
|
||||
await initRundown(persistedRundown, persistedCustomFields);
|
||||
initRundown(persistedRundown, persistedCustomFields);
|
||||
|
||||
// initialise message service
|
||||
messageService.init(eventStore.set, eventStore.get);
|
||||
@@ -235,8 +235,6 @@ export const startIntegrations = async () => {
|
||||
const { enabledOscIn, oscPortIn } = getDataProvider().getAutomation();
|
||||
if (enabledOscIn) {
|
||||
oscServer.init(oscPortIn);
|
||||
} else {
|
||||
logger.info(LogOrigin.Server, 'Skipping OSC integration');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -114,13 +114,18 @@ async function getDeviceCodes(clientSecret: ClientSecret): Promise<CodesResponse
|
||||
|
||||
/**
|
||||
* Gets credentials from Google Auth server
|
||||
* @param clientSecret
|
||||
* @param device_code
|
||||
* @param interval
|
||||
* @param expires_in
|
||||
* @param postAction
|
||||
*/
|
||||
function verifyConnection(
|
||||
clientSecret: ClientSecret,
|
||||
device_code: string,
|
||||
interval: number,
|
||||
expires_in: number,
|
||||
postAction: () => Promise<any>,
|
||||
postAction: () => void,
|
||||
) {
|
||||
// create poller to check for auth
|
||||
pollInterval = setInterval(pollForAuth, interval * 1000);
|
||||
|
||||
@@ -6,14 +6,14 @@ import { publicDir, publicFiles, srcDir, srcFiles } from './index.js';
|
||||
/**
|
||||
* @description ensures directories exist and populates demo folder
|
||||
*/
|
||||
export async function populateDemo() {
|
||||
export const populateDemo = () => {
|
||||
ensureDirectory(publicDir.demoDir);
|
||||
|
||||
try {
|
||||
copyFileSync(srcFiles.externalReadme, publicFiles.externalReadme);
|
||||
// even if demo exist we want to use startup demo
|
||||
await copyDirectory(srcDir.demoDir, publicDir.demoDir);
|
||||
copyDirectory(srcDir.demoDir, publicDir.demoDir);
|
||||
} catch (_) {
|
||||
/* we do not handle this */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const eventStore = {
|
||||
for (const dataKey of changedKeys) {
|
||||
socket.sendAsJson({ type: `ontime-${dataKey}`, payload: store[dataKey] });
|
||||
}
|
||||
socket.sendAsJson({ type: 'ontime-flush' });
|
||||
socket.sendAsJson({ type: `ontime-flush` });
|
||||
isUpdatePending = null;
|
||||
changedKeys.clear();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isProduction } from '../externals.js';
|
||||
|
||||
import { consoleError } from './console.js';
|
||||
import { consoleError } from '../utils/console.js';
|
||||
|
||||
/**
|
||||
* Milestone checker for dev environment
|
||||
|
||||
@@ -67,11 +67,11 @@ export function mergeObject<T extends object>(a: T, b: Partial<T>): T {
|
||||
* @param {object} obj
|
||||
*/
|
||||
export const removeUndefined = <T extends Record<string, unknown>>(obj: T): Partial<T> => {
|
||||
return Object.keys(obj).reduce<Partial<T>>((patched, key) => {
|
||||
return Object.keys(obj).reduce((patched, key) => {
|
||||
if (typeof obj[key] !== 'undefined') {
|
||||
// @ts-expect-error -- not sure how to type this
|
||||
patched[key] = obj[key];
|
||||
}
|
||||
return patched;
|
||||
}, {});
|
||||
}, {} as Partial<T>);
|
||||
};
|
||||
|
||||
@@ -84,9 +84,9 @@ test('delays are show correctly', async ({ page }) => {
|
||||
|
||||
// delay is NOT shown in the public view
|
||||
await page.goto('http://localhost:4001/public');
|
||||
await page.getByText('00:10→00:20').click();
|
||||
await page.getByText('00:10 → 00:20').click();
|
||||
|
||||
// delay is shown in the backstage view
|
||||
await page.goto('http://localhost:4001/backstage');
|
||||
await page.getByText('00:11→00:21').click();
|
||||
await page.getByText('00:11 → 00:21').click();
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.12.0",
|
||||
"version": "3.11.1-beta.1",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"ontime",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SecondarySource } from '../runtime/MessageControl.type.js';
|
||||
import type { TimerLifeCycle } from './TimerLifecycle.type.js';
|
||||
|
||||
export type AutomationSettings = {
|
||||
@@ -39,7 +38,7 @@ export type AutomationFilter = {
|
||||
value: string; // we use string but would coerce to the field value
|
||||
};
|
||||
|
||||
export type AutomationOutput = OSCOutput | HTTPOutput | OntimeAction;
|
||||
export type AutomationOutput = OSCOutput | HTTPOutput;
|
||||
|
||||
export type OSCOutput = {
|
||||
type: 'osc';
|
||||
@@ -53,25 +52,3 @@ export type HTTPOutput = {
|
||||
type: 'http';
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type OntimeAction =
|
||||
| {
|
||||
type: 'ontime';
|
||||
action: 'aux-start' | 'aux-stop' | 'aux-pause';
|
||||
}
|
||||
| {
|
||||
type: 'ontime';
|
||||
action: 'aux-set';
|
||||
time: number;
|
||||
}
|
||||
| {
|
||||
type: 'ontime';
|
||||
action: 'message-set';
|
||||
text?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'ontime';
|
||||
action: 'message-secondary';
|
||||
secondarySource: SecondarySource;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
export type SecondarySource = 'aux' | 'external' | null;
|
||||
|
||||
export type TimerMessage = {
|
||||
text: string;
|
||||
visible: boolean;
|
||||
blink: boolean;
|
||||
blackout: boolean;
|
||||
secondarySource: SecondarySource;
|
||||
secondarySource: 'aux' | 'external' | null;
|
||||
};
|
||||
|
||||
export type MessageState = {
|
||||
|
||||
@@ -26,7 +26,6 @@ export type {
|
||||
FilterRule,
|
||||
HTTPOutput,
|
||||
NormalisedAutomation,
|
||||
OntimeAction,
|
||||
OSCOutput,
|
||||
Trigger,
|
||||
TriggerDTO,
|
||||
@@ -81,7 +80,7 @@ export type {
|
||||
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
||||
export { Playback } from './definitions/runtime/Playback.type.js';
|
||||
export { TimerLifeCycle, timerLifecycleValues } from './definitions/core/TimerLifecycle.type.js';
|
||||
export type { TimerMessage, MessageState, SecondarySource } from './definitions/runtime/MessageControl.type.js';
|
||||
export type { TimerMessage, MessageState } from './definitions/runtime/MessageControl.type.js';
|
||||
|
||||
export type { Runtime } from './definitions/runtime/Runtime.type.js';
|
||||
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||
@@ -105,6 +104,5 @@ export {
|
||||
isKeyOfType,
|
||||
isOSCOutput,
|
||||
isHTTPOutput,
|
||||
isOntimeAction,
|
||||
} from './utils/guards.js';
|
||||
export type { MaybeNumber, MaybeString } from './utils/utils.type.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AutomationOutput, HTTPOutput, OntimeAction, OSCOutput } from '../definitions/core/Automation.type.js';
|
||||
import type { AutomationOutput, HTTPOutput, OSCOutput } from '../definitions/core/Automation.type.js';
|
||||
import type { OntimeBlock, OntimeDelay, OntimeEvent, PlayableEvent } from '../definitions/core/OntimeEvent.type.js';
|
||||
import { SupportedEvent } from '../definitions/core/OntimeEvent.type.js';
|
||||
import type { OntimeRundownEntry } from '../definitions/core/Rundown.type.js';
|
||||
@@ -41,7 +41,3 @@ export function isOSCOutput(output: AutomationOutput): output is OSCOutput {
|
||||
export function isHTTPOutput(output: AutomationOutput): output is HTTPOutput {
|
||||
return output.type === 'http';
|
||||
}
|
||||
|
||||
export function isOntimeAction(output: AutomationOutput): output is OntimeAction {
|
||||
return output.type === 'ontime';
|
||||
}
|
||||
|
||||
Generated
+99
-165
@@ -168,8 +168,8 @@ importers:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
zustand:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1))
|
||||
specifier: ^4.5.2
|
||||
version: 4.5.2(@types/react@18.0.26)(react@18.3.1)
|
||||
devDependencies:
|
||||
'@sentry/vite-plugin':
|
||||
specifier: ^2.16.1
|
||||
@@ -257,7 +257,7 @@ importers:
|
||||
version: 31.2.0
|
||||
electron-builder:
|
||||
specifier: ^24.13.3
|
||||
version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
version: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 8.56.0
|
||||
@@ -2261,8 +2261,8 @@ packages:
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||
acorn-walk@8.3.2:
|
||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.11.2:
|
||||
@@ -2270,11 +2270,6 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.14.0:
|
||||
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
adler-32@1.3.1:
|
||||
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -2380,9 +2375,6 @@ packages:
|
||||
async@3.2.5:
|
||||
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@@ -2504,10 +2496,6 @@ packages:
|
||||
resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bind@1.0.2:
|
||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||
|
||||
@@ -2748,8 +2736,8 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.5.0:
|
||||
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||
decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
@@ -2920,18 +2908,10 @@ packages:
|
||||
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.0.1:
|
||||
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-shim-unscopables@1.0.0:
|
||||
resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
|
||||
|
||||
@@ -2972,8 +2952,8 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
escodegen@2.1.0:
|
||||
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
|
||||
escodegen@2.0.0:
|
||||
resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==}
|
||||
engines: {node: '>=6.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3200,10 +3180,6 @@ packages:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.2:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3295,18 +3271,10 @@ packages:
|
||||
resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-intrinsic@1.2.7:
|
||||
resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-stream@5.2.0:
|
||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3428,10 +3396,6 @@ packages:
|
||||
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has@1.0.3:
|
||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
@@ -3723,6 +3687,10 @@ packages:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
|
||||
levn@0.3.0:
|
||||
resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3959,8 +3927,8 @@ packages:
|
||||
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
nwsapi@2.2.16:
|
||||
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
|
||||
nwsapi@2.2.2:
|
||||
resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
@@ -4007,6 +3975,10 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
optionator@0.8.3:
|
||||
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4039,8 +4011,8 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse5@7.2.1:
|
||||
resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
|
||||
parse5@7.1.2:
|
||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
@@ -4107,6 +4079,10 @@ packages:
|
||||
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prelude-ls@1.1.2:
|
||||
resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4141,8 +4117,8 @@ packages:
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
psl@1.15.0:
|
||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||
psl@1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
|
||||
pump@3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
@@ -4151,10 +4127,6 @@ packages:
|
||||
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qr.js@0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
|
||||
@@ -4555,9 +4527,6 @@ packages:
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4655,8 +4624,8 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
tough-cookie@4.1.2:
|
||||
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tr46@0.0.3:
|
||||
@@ -4747,6 +4716,10 @@ packages:
|
||||
resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==}
|
||||
hasBin: true
|
||||
|
||||
type-check@0.3.2:
|
||||
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5018,6 +4991,10 @@ packages:
|
||||
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
word-wrap@1.2.3:
|
||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
word@0.3.0:
|
||||
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -5094,14 +5071,13 @@ packages:
|
||||
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
zustand@5.0.3:
|
||||
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
zustand@4.5.2:
|
||||
resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
'@types/react': '>=16.8'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
@@ -5109,8 +5085,6 @@ packages:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
@@ -7091,24 +7065,19 @@ snapshots:
|
||||
|
||||
acorn-globals@7.0.1:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
acorn-walk: 8.3.4
|
||||
acorn: 8.11.2
|
||||
acorn-walk: 8.3.2
|
||||
optional: true
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.11.2):
|
||||
dependencies:
|
||||
acorn: 8.11.2
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
acorn-walk@8.3.2:
|
||||
optional: true
|
||||
|
||||
acorn@8.11.2: {}
|
||||
|
||||
acorn@8.14.0:
|
||||
optional: true
|
||||
|
||||
adler-32@1.3.1: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
@@ -7151,7 +7120,7 @@ snapshots:
|
||||
|
||||
app-builder-bin@4.0.0: {}
|
||||
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3):
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
'@develar/schema-utils': 2.6.5
|
||||
'@electron/notarize': 2.2.1
|
||||
@@ -7216,7 +7185,7 @@ snapshots:
|
||||
archiver@5.3.2:
|
||||
dependencies:
|
||||
archiver-utils: 2.1.0
|
||||
async: 3.2.6
|
||||
async: 3.2.5
|
||||
buffer-crc32: 0.2.13
|
||||
readable-stream: 3.6.2
|
||||
readdir-glob: 1.1.3
|
||||
@@ -7268,8 +7237,6 @@ snapshots:
|
||||
|
||||
async@3.2.5: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
at-least-node@1.0.0: {}
|
||||
@@ -7430,12 +7397,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
optional: true
|
||||
|
||||
call-bind@1.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -7679,7 +7640,7 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.5.0:
|
||||
decimal.js@10.4.3:
|
||||
optional: true
|
||||
|
||||
decompress-response@6.0.0:
|
||||
@@ -7728,7 +7689,7 @@ snapshots:
|
||||
|
||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
fs-extra: 10.1.0
|
||||
@@ -7794,7 +7755,7 @@ snapshots:
|
||||
|
||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
archiver: 5.3.2
|
||||
builder-util: 24.13.1
|
||||
fs-extra: 10.1.0
|
||||
@@ -7802,9 +7763,9 @@ snapshots:
|
||||
- dmg-builder
|
||||
- supports-color
|
||||
|
||||
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
|
||||
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
chalk: 4.1.2
|
||||
@@ -7907,25 +7868,12 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
optional: true
|
||||
|
||||
es-set-tostringtag@2.0.1:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.2
|
||||
has: 1.0.3
|
||||
has-tostringtag: 1.0.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.2.7
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
optional: true
|
||||
|
||||
es-shim-unscopables@1.0.0:
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
@@ -8027,11 +7975,12 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
escodegen@2.1.0:
|
||||
escodegen@2.0.0:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
estraverse: 5.3.0
|
||||
esutils: 2.0.3
|
||||
optionator: 0.8.3
|
||||
optionalDependencies:
|
||||
source-map: 0.6.1
|
||||
optional: true
|
||||
@@ -8327,14 +8276,6 @@ snapshots:
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.2:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
mime-types: 2.1.35
|
||||
optional: true
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
frac@1.1.2: {}
|
||||
@@ -8439,28 +8380,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-intrinsic@1.2.7:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
optional: true
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
optional: true
|
||||
|
||||
get-stream@5.2.0:
|
||||
dependencies:
|
||||
pump: 3.0.0
|
||||
@@ -8629,11 +8550,6 @@ snapshots:
|
||||
dependencies:
|
||||
has-symbols: 1.0.3
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
optional: true
|
||||
|
||||
has@1.0.3:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -8872,24 +8788,24 @@ snapshots:
|
||||
jsdom@21.1.0:
|
||||
dependencies:
|
||||
abab: 2.0.6
|
||||
acorn: 8.14.0
|
||||
acorn: 8.11.2
|
||||
acorn-globals: 7.0.1
|
||||
cssom: 0.5.0
|
||||
cssstyle: 2.3.0
|
||||
data-urls: 3.0.2
|
||||
decimal.js: 10.5.0
|
||||
decimal.js: 10.4.3
|
||||
domexception: 4.0.0
|
||||
escodegen: 2.1.0
|
||||
form-data: 4.0.2
|
||||
escodegen: 2.0.0
|
||||
form-data: 4.0.0
|
||||
html-encoding-sniffer: 3.0.0
|
||||
http-proxy-agent: 5.0.0
|
||||
https-proxy-agent: 5.0.1
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.16
|
||||
parse5: 7.2.1
|
||||
nwsapi: 2.2.2
|
||||
parse5: 7.1.2
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 4.1.4
|
||||
tough-cookie: 4.1.2
|
||||
w3c-xmlserializer: 4.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 2.0.0
|
||||
@@ -8958,6 +8874,12 @@ snapshots:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
|
||||
levn@0.3.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.1.2
|
||||
type-check: 0.3.2
|
||||
optional: true
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -9138,7 +9060,7 @@ snapshots:
|
||||
|
||||
normalize-url@8.0.1: {}
|
||||
|
||||
nwsapi@2.2.16:
|
||||
nwsapi@2.2.2:
|
||||
optional: true
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
@@ -9189,6 +9111,16 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
optionator@0.8.3:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
fast-levenshtein: 2.0.6
|
||||
levn: 0.3.0
|
||||
prelude-ls: 1.1.2
|
||||
type-check: 0.3.2
|
||||
word-wrap: 1.2.3
|
||||
optional: true
|
||||
|
||||
optionator@0.9.3:
|
||||
dependencies:
|
||||
'@aashutoshrathi/word-wrap': 1.2.6
|
||||
@@ -9223,7 +9155,7 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse5@7.2.1:
|
||||
parse5@7.1.2:
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
optional: true
|
||||
@@ -9277,6 +9209,9 @@ snapshots:
|
||||
picocolors: 1.0.1
|
||||
source-map-js: 1.2.0
|
||||
|
||||
prelude-ls@1.1.2:
|
||||
optional: true
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.0:
|
||||
@@ -9307,9 +9242,7 @@ snapshots:
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
psl@1.15.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
psl@1.9.0:
|
||||
optional: true
|
||||
|
||||
pump@3.0.0:
|
||||
@@ -9319,9 +9252,6 @@ snapshots:
|
||||
|
||||
punycode@2.1.1: {}
|
||||
|
||||
punycode@2.3.1:
|
||||
optional: true
|
||||
|
||||
qr.js@0.0.0: {}
|
||||
|
||||
qs@6.11.0:
|
||||
@@ -9460,7 +9390,7 @@ snapshots:
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdir-glob@1.1.3:
|
||||
@@ -9782,10 +9712,6 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
@@ -9876,10 +9802,10 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
tough-cookie@4.1.2:
|
||||
dependencies:
|
||||
psl: 1.15.0
|
||||
punycode: 2.3.1
|
||||
psl: 1.9.0
|
||||
punycode: 2.1.1
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
optional: true
|
||||
@@ -9888,7 +9814,7 @@ snapshots:
|
||||
|
||||
tr46@3.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
punycode: 2.1.1
|
||||
optional: true
|
||||
|
||||
truncate-utf8-bytes@1.0.2:
|
||||
@@ -9952,6 +9878,11 @@ snapshots:
|
||||
turbo-windows-64: 2.3.3
|
||||
turbo-windows-arm64: 2.3.3
|
||||
|
||||
type-check@0.3.2:
|
||||
dependencies:
|
||||
prelude-ls: 1.1.2
|
||||
optional: true
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -10045,7 +9976,6 @@ snapshots:
|
||||
use-sync-external-store@1.2.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optional: true
|
||||
|
||||
utf8-byte-length@1.0.4: {}
|
||||
|
||||
@@ -10293,6 +10223,9 @@ snapshots:
|
||||
|
||||
wmf@1.0.2: {}
|
||||
|
||||
word-wrap@1.2.3:
|
||||
optional: true
|
||||
|
||||
word@0.3.0: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
@@ -10358,8 +10291,9 @@ snapshots:
|
||||
compress-commons: 4.1.2
|
||||
readable-stream: 3.6.2
|
||||
|
||||
zustand@5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)):
|
||||
zustand@4.5.2(@types/react@18.0.26)(react@18.3.1):
|
||||
dependencies:
|
||||
use-sync-external-store: 1.2.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.0.26
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.2.0(react@18.3.1)
|
||||
|
||||
Reference in New Issue
Block a user