mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 06:58:02 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da829dc09f | |||
| c4d65dbe5f | |||
| 2586b0b10c | |||
| abe27d54a2 | |||
| 1343818917 | |||
| a93cd3e9b1 | |||
| 813eb9ad86 | |||
| 34e9b1ef07 | |||
| a67b89190d | |||
| ecd151a0a8 | |||
| 4cbc09864c | |||
| 20d5d8b129 | |||
| 11db39ee73 | |||
| edb79d5819 | |||
| 715fa66444 | |||
| 27f6d10677 | |||
| f2504072ce | |||
| 9e285ee8f3 | |||
| 1ccf2108eb | |||
| db56ac8049 | |||
| 0660f014c8 | |||
| 41c26778cb | |||
| b11041938d | |||
| e6b4f537af | |||
| 8cabb347e9 | |||
| 22d89c6fbc | |||
| facce4f096 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "2.16.2",
|
||||
"version": "2.24.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.7.0",
|
||||
|
||||
@@ -5,6 +5,7 @@ export const USERFIELDS = ['userFields'];
|
||||
export const RUNDOWN = ['rundown'];
|
||||
export const APP_INFO = ['appinfo'];
|
||||
export const OSC_SETTINGS = ['oscSettings'];
|
||||
export const HTTP_SETTINGS = ['httpSettings'];
|
||||
export const APP_SETTINGS = ['appSettings'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
export const RUNTIME = ['runtimeStore'];
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
Alias,
|
||||
DatabaseModel,
|
||||
GetInfo,
|
||||
GoogleSheet,
|
||||
GoogleSheetState,
|
||||
HttpSettings,
|
||||
OntimeRundown,
|
||||
OSCSettings,
|
||||
OscSubscription,
|
||||
@@ -106,6 +105,23 @@ export async function getOSC(): Promise<OSCSettings> {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve http settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getHTTP(): Promise<HttpSettings> {
|
||||
const res = await axios.get(`${ontimeURL}/http`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate http settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postHTTP(data: HttpSettings) {
|
||||
return axios.post(`${ontimeURL}/http`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate osc settings
|
||||
* @return {Promise}
|
||||
@@ -229,62 +245,3 @@ export async function getLatestVersion(): Promise<HasUpdate> {
|
||||
export async function postNew(initialData: Partial<ProjectData>) {
|
||||
return axios.post(`${ontimeURL}/new`, initialData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description sheet Client File
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadSheetClientFile = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
await axios
|
||||
.post(`${ontimeURL}/sheet-clientsecrect`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
};
|
||||
|
||||
export const getSheetsAuthUrl = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/sheet-authurl`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postPreviewSheet = async () => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-preview`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const postPushSheet = async () => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-push`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve google sheets settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getSheetSettings(): Promise<GoogleSheet> {
|
||||
const res = await axios.get(`${ontimeURL}/sheet-settings`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate google sheets settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postSheetSettings(data: GoogleSheet): Promise<GoogleSheet> {
|
||||
const res = await axios.post(`${ontimeURL}/sheet-settings`, data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve google sheets state
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getSheetstate(): Promise<GoogleSheetState> {
|
||||
const res = await axios.get(`${ontimeURL}/sheet-state`);
|
||||
return res.data;
|
||||
}
|
||||
@@ -212,6 +212,7 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
|
||||
@@ -10,10 +10,6 @@ $icon-color: $ui-white;
|
||||
$button-bg: $gray-1050;
|
||||
$button-size: 48px;
|
||||
|
||||
.mirror {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import useClickOutside from '../../hooks/useClickOutside';
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
|
||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||
@@ -23,18 +22,19 @@ function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||
const { toggleMirror } = useViewOptionsStore();
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useClickOutside(menuRef, () => setShowMenu(false));
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' || isOpen });
|
||||
|
||||
// show on mouse move
|
||||
useEffect(() => {
|
||||
let fadeOut: NodeJS.Timeout | null = null;
|
||||
const setShowMenuTrue = () => {
|
||||
@@ -63,7 +63,7 @@ function NavigationMenu() {
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
||||
<div id='navigation-menu-portal' ref={menuRef}>
|
||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Input, Select, Switch } from '@chakra-ui/react';
|
||||
import { Input, InputGroup, InputLeftElement, Select, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { isStringBoolean } from '../../utils/viewUtils';
|
||||
|
||||
@@ -9,16 +9,22 @@ interface EditFormInputProps {
|
||||
paramField: ParamField;
|
||||
}
|
||||
|
||||
export default function ParamInput({ paramField }: EditFormInputProps) {
|
||||
export default function ParamInput(props: EditFormInputProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { id, type } = paramField;
|
||||
const { paramField } = props;
|
||||
const { id, type, defaultValue } = paramField;
|
||||
|
||||
if (type === 'option') {
|
||||
const optionFromParams = searchParams.get(id);
|
||||
const defaultOptionValue = optionFromParams || undefined;
|
||||
const defaultOptionValue = optionFromParams || defaultValue;
|
||||
|
||||
return (
|
||||
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
|
||||
<Select
|
||||
placeholder={defaultValue ? undefined : 'Select an option'}
|
||||
variant='ontime'
|
||||
name={id}
|
||||
defaultValue={defaultOptionValue}
|
||||
>
|
||||
{Object.entries(paramField.values).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{value}
|
||||
@@ -29,19 +35,38 @@ export default function ParamInput({ paramField }: EditFormInputProps) {
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
|
||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue;
|
||||
|
||||
// checked value should be 'true', so it can be captured by the form event
|
||||
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const defaultNumberValue = searchParams.get(id) ?? '';
|
||||
const { prefix, placeholder } = paramField;
|
||||
const defaultNumberValue = searchParams.get(id) ?? defaultValue;
|
||||
|
||||
return <Input type='number' step='any' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
|
||||
return (
|
||||
<InputGroup variant='ontime-filled'>
|
||||
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||
<Input
|
||||
type='number'
|
||||
step='any'
|
||||
variant='ontime-filled'
|
||||
name={id}
|
||||
defaultValue={defaultNumberValue}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultStringValue = searchParams.get(id) ?? '';
|
||||
const defaultStringValue = searchParams.get(id) ?? defaultValue;
|
||||
const { prefix, placeholder } = paramField;
|
||||
|
||||
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
|
||||
return (
|
||||
<InputGroup variant='ontime-filled'>
|
||||
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||
<Input name={id} defaultValue={defaultStringValue} placeholder={placeholder} />
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,15 @@ import style from './ViewParamsEditor.module.scss';
|
||||
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
||||
type SavedViewParams = Record<string, ViewParamsObj>;
|
||||
|
||||
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
|
||||
Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
||||
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ParamField[]) => {
|
||||
const defaultValues = paramFields.map(({ defaultValue }) => String(defaultValue));
|
||||
|
||||
return Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
||||
if (typeof value === 'string' && value.length) {
|
||||
if (defaultValues.includes(value)) {
|
||||
return newSearchParams;
|
||||
}
|
||||
|
||||
newSearchParams.set(id, value);
|
||||
|
||||
return newSearchParams;
|
||||
@@ -32,6 +38,7 @@ const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
|
||||
|
||||
return newSearchParams;
|
||||
}, new URLSearchParams());
|
||||
};
|
||||
|
||||
interface EditFormDrawerProps {
|
||||
paramFields: ParamField[];
|
||||
@@ -72,31 +79,30 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
|
||||
*/
|
||||
|
||||
const onEditDrawerClose = () => {
|
||||
const onCloseWithoutSaving = () => {
|
||||
onClose();
|
||||
|
||||
searchParams.delete('edit');
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const clearParams = () => {
|
||||
const resetParams = () => {
|
||||
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
|
||||
setSearchParams();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||
formEvent.preventDefault();
|
||||
|
||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject);
|
||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
||||
|
||||
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
|
||||
setSearchParams(newSearchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
|
||||
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerHeader className={style.drawerHeader}>
|
||||
@@ -119,10 +125,10 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
</DrawerBody>
|
||||
|
||||
<DrawerFooter className={style.drawerFooter}>
|
||||
<Button variant='ontime-ghosted' onClick={clearParams} type='reset'>
|
||||
Clear
|
||||
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
|
||||
<Button variant='ontime-subtle' onClick={onCloseWithoutSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||
|
||||
@@ -1,46 +1,56 @@
|
||||
import { UserFields } from 'ontime-types';
|
||||
import { TimeFormat } from 'ontime-types/src/definitions/core/TimeFormat.type';
|
||||
|
||||
import { ParamField } from './types';
|
||||
|
||||
export const TIME_FORMAT_OPTION: ParamField = {
|
||||
export const getTimeOption = (timeFormat: TimeFormat): ParamField => ({
|
||||
id: 'format',
|
||||
title: '12 / 24 hour timer',
|
||||
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
||||
type: 'option',
|
||||
values: { '12': '12 hour AM/PM', '24': '24 hour' },
|
||||
};
|
||||
defaultValue: timeFormat,
|
||||
});
|
||||
|
||||
export const CLOCK_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
export const getClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'fffff (default)',
|
||||
},
|
||||
{
|
||||
id: 'textbg',
|
||||
title: 'Text Background',
|
||||
description: 'Colour of text background in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'font',
|
||||
title: 'Font',
|
||||
description: 'Font family, will use the fonts available in the system',
|
||||
type: 'string',
|
||||
placeholder: 'Arial Black (default)',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Text Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
placeholder: '1 (default)',
|
||||
},
|
||||
{
|
||||
id: 'alignx',
|
||||
@@ -48,12 +58,14 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
id: 'offsetx',
|
||||
title: 'Offset Horizontal',
|
||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||
type: 'number',
|
||||
placeholder: '0 (default)',
|
||||
},
|
||||
{
|
||||
id: 'aligny',
|
||||
@@ -61,46 +73,53 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
id: 'offsety',
|
||||
title: 'Offset Vertical',
|
||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||
type: 'number',
|
||||
placeholder: '0 (default)',
|
||||
},
|
||||
];
|
||||
|
||||
export const TIMER_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
export const getTimerOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hideClock',
|
||||
title: 'Hide Time Now',
|
||||
description: 'Hides the Time Now field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideCards',
|
||||
title: 'Hide Cards',
|
||||
description: 'Hides the Now and Next cards',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideProgress',
|
||||
title: 'Hide progress bar',
|
||||
description: 'Hides the progress bar',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideMessage',
|
||||
title: 'Hide Presenter Message',
|
||||
description: 'Prevents the screen from displaying messages from the presenter',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideExternal',
|
||||
title: 'Hide External',
|
||||
description: 'Prevents the screen from displaying the external field',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -109,31 +128,39 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'fffff (default)',
|
||||
},
|
||||
{
|
||||
id: 'textbg',
|
||||
title: 'Text Background',
|
||||
description: 'Colour of text background in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000000 (default)',
|
||||
},
|
||||
{
|
||||
id: 'font',
|
||||
title: 'Font',
|
||||
description: 'Font family, will use the fonts available in the system',
|
||||
type: 'string',
|
||||
placeholder: 'Arial Black (default)',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Text Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
placeholder: '1 (default)',
|
||||
},
|
||||
{
|
||||
id: 'alignx',
|
||||
@@ -141,12 +168,14 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
id: 'offsetx',
|
||||
title: 'Offset Horizontal',
|
||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||
type: 'number',
|
||||
placeholder: '0 (default)',
|
||||
},
|
||||
{
|
||||
id: 'aligny',
|
||||
@@ -154,145 +183,162 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
id: 'offsety',
|
||||
title: 'Offset Vertical',
|
||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||
type: 'number',
|
||||
placeholder: '0 (default)',
|
||||
},
|
||||
{
|
||||
id: 'hideovertime',
|
||||
title: 'Hide Overtime',
|
||||
description: 'Whether to suppress overtime styles (red borders and red text)',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hidemessages',
|
||||
title: 'Hide Message Overlay',
|
||||
description: 'Whether to hide the overlay from showing timer screen messages',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideendmessage',
|
||||
title: 'Hide End Message',
|
||||
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
||||
{
|
||||
id: 'preset',
|
||||
title: 'Preset',
|
||||
description: 'Selects a style preset (0-1)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
placeholder: '1 (default)',
|
||||
},
|
||||
{
|
||||
id: 'transition',
|
||||
title: 'Transition',
|
||||
description: 'Transition in time in seconds (default 5)',
|
||||
description: 'Transition in time in seconds (default 3)',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: 'fffffa (default)',
|
||||
},
|
||||
{
|
||||
id: 'bg',
|
||||
title: 'Text Background',
|
||||
description: 'Text background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000033 (default)',
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Screen background colour in hexadecimal',
|
||||
prefix: '#',
|
||||
type: 'string',
|
||||
placeholder: '00000033 (default)',
|
||||
},
|
||||
{
|
||||
id: 'fadeout',
|
||||
title: 'Fadeout',
|
||||
description: 'Time (in seconds) the lower third displays before fading out',
|
||||
type: 'number',
|
||||
placeholder: '3 (default)',
|
||||
},
|
||||
];
|
||||
|
||||
export const BACKSTAGE_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
export const getBackstageOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
];
|
||||
|
||||
export const PUBLIC_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
export const getPublicOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
description: 'Scheduler will only show upcoming events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'stopCycle',
|
||||
title: 'Stop cycling through event pages',
|
||||
description: 'Schedule will not auto-cycle through events',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'eventsPerPage',
|
||||
title: 'Events per page',
|
||||
description: 'Sets the number of events on the page, can cause overlow',
|
||||
type: 'number',
|
||||
placeholder: '7 (default)',
|
||||
},
|
||||
];
|
||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
export const getStudioClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'seconds',
|
||||
title: 'Show Seconds',
|
||||
description: 'Shows seconds in clock',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const getOperatorOptions = (userFields: UserFields): ParamField[] => {
|
||||
export const getOperatorOptions = (userFields: UserFields, timeFormat: TimeFormat): ParamField[] => {
|
||||
return [
|
||||
TIME_FORMAT_OPTION,
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
id: 'showseconds',
|
||||
title: 'Show seconds',
|
||||
description: 'Schedule shows hh:mm:ss',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hidepast',
|
||||
title: 'Hide Past Events',
|
||||
description: 'Whether to events that have passed',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'main',
|
||||
|
||||
@@ -4,9 +4,13 @@ type BaseField = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
type OptionsField = { type: 'option'; values: Record<string, string> };
|
||||
type StringField = { type: 'string' };
|
||||
type BooleanField = { type: 'boolean' };
|
||||
type NumberField = { type: 'number' };
|
||||
type OptionsField = {
|
||||
type: 'option';
|
||||
values: Record<string, string>;
|
||||
defaultValue?: string;
|
||||
};
|
||||
type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string };
|
||||
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
||||
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
||||
|
||||
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { HTTP_SETTINGS } from '../api/apiConstants';
|
||||
import { logAxiosError } from '../api/apiUtils';
|
||||
import { getHTTP, postHTTP } from '../api/ontimeApi';
|
||||
import { httpPlaceholder } from '../models/Http';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
export function useHttpSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: HTTP_SETTINGS,
|
||||
queryFn: getHTTP,
|
||||
placeholderData: httpPlaceholder,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
// we need to jump through some hoops because of the type op port
|
||||
return { data: data! as unknown as HttpSettings, status, isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function usePostHttpSettings() {
|
||||
const { isPending, mutateAsync } = useMutation({
|
||||
mutationFn: postHTTP,
|
||||
onError: (error) => logAxiosError('Error saving HTTP settings', error),
|
||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: HTTP_SETTINGS }),
|
||||
});
|
||||
return { isPending, mutateAsync };
|
||||
}
|
||||
@@ -75,8 +75,8 @@ export const setPlayback = {
|
||||
reload: () => {
|
||||
socketSendJson('reload');
|
||||
},
|
||||
delay: (amount: number) => {
|
||||
socketSendJson('delay', amount);
|
||||
addTime: (amount: number) => {
|
||||
socketSendJson('addtime', amount);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
export const httpPlaceholder = {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
|
||||
export const httpPlaceholder: HttpSettings = {
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: [],
|
||||
onStart: [],
|
||||
onUpdate: [],
|
||||
onPause: [],
|
||||
onStop: [],
|
||||
onFinish: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isIPAddress, isOnlyNumbers } from '../regex';
|
||||
import { isIPAddress, isOnlyNumbers, startsWithHttp } from '../regex';
|
||||
|
||||
describe('simple tests for regex', () => {
|
||||
test('isOnlyNumbers', () => {
|
||||
@@ -24,4 +24,16 @@ describe('simple tests for regex', () => {
|
||||
expect(isIPAddress.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('startsWithHttp', () => {
|
||||
const right = ['http://test'];
|
||||
const wrong = ['https://test', 'testing', '123.0.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(startsWithHttp.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(startsWithHttp.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cx } from '../styleUtils';
|
||||
import { cx, getAccessibleColour } from '../styleUtils';
|
||||
|
||||
import style from './styleUtils.module.scss';
|
||||
|
||||
@@ -13,3 +13,24 @@ describe('cx()', () => {
|
||||
expect(merged).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessibleColour()', () => {
|
||||
it('handles named colours', () => {
|
||||
const colour = 'red';
|
||||
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||
expect(backgroundColor).toBe('#FF0000FF');
|
||||
expect(color).toBe('#fffffa');
|
||||
});
|
||||
it('handles hex colours', () => {
|
||||
const colour = '#0F0';
|
||||
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||
expect(backgroundColor).toBe('#00FF00FF');
|
||||
expect(color).toBe('black');
|
||||
});
|
||||
it('handles transparens', () => {
|
||||
const colour = '#0F08';
|
||||
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||
expect(backgroundColor).toBe('#0C940CFF');
|
||||
expect(color).toBe('#fffffa');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
export const startsWithHttp = /^http:\/\//;
|
||||
|
||||
@@ -13,13 +13,15 @@ type ColourCombination = {
|
||||
export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
||||
if (bgColour) {
|
||||
try {
|
||||
const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa';
|
||||
return { backgroundColor: bgColour, color: textColor };
|
||||
const originalColour = Color(bgColour);
|
||||
const backgroundColorMix = originalColour.alpha(1).mix(Color('#1a1a1a'), 1 - originalColour.alpha());
|
||||
const textColor = backgroundColorMix.isLight() ? 'black' : '#fffffa';
|
||||
return { backgroundColor: backgroundColorMix.hexa(), color: textColor };
|
||||
} catch (_error) {
|
||||
/* we do not handle errors here */
|
||||
}
|
||||
}
|
||||
return { backgroundColor: '#000', color: '#fffffa' };
|
||||
return { backgroundColor: '#1a1a1a', color: '#fffffa' };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,22 +86,22 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(-1)} disabled={disableButtons} aspect='square'>
|
||||
<TapButton onClick={() => setPlayback.addTime(-60)} disabled={disableButtons} aspect='square'>
|
||||
-1
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(1)} disabled={disableButtons} aspect='square'>
|
||||
<TapButton onClick={() => setPlayback.addTime(60)} disabled={disableButtons} aspect='square'>
|
||||
+1
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(-5)} disabled={disableButtons} aspect='square'>
|
||||
<TapButton onClick={() => setPlayback.addTime(-5 * 60)} disabled={disableButtons} aspect='square'>
|
||||
-5
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(+5)} disabled={disableButtons} aspect='square'>
|
||||
<TapButton onClick={() => setPlayback.addTime(+5 * 60)} disabled={disableButtons} aspect='square'>
|
||||
+5
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundo
|
||||
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||
import { getAccessibleColour } from '../../common/utils/styleUtils';
|
||||
|
||||
import BlockRow from './cuesheet-table-elements/BlockRow';
|
||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||
@@ -120,8 +121,8 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
} else if (row.original.colour) {
|
||||
try {
|
||||
// the colour is user defined and might be invalid
|
||||
const colour = new Color(row.original.colour).alpha(0.25);
|
||||
rowBgColour = colour.hsl().string();
|
||||
const accessibleBackgroundColor = Color(getAccessibleColour(row.original.colour).backgroundColor);
|
||||
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
||||
} catch (_error) {
|
||||
/* we do not handle errors here */
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const bgColour = colour;
|
||||
const textColour = getAccessibleColour(bgColour);
|
||||
const textColour = getAccessibleColour(colour);
|
||||
const bgColour = textColour.backgroundColor;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
@@ -55,7 +55,7 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
style={{ opacity: `${isPast ? pastOpacity : '1'}` }}
|
||||
ref={selectedRef ?? ownRef}
|
||||
>
|
||||
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour?.color }}>
|
||||
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour.color }}>
|
||||
{eventIndex}
|
||||
</td>
|
||||
{isVisible ? children : null}
|
||||
|
||||
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import AboutModal from '../modals/about-modal/AboutModal';
|
||||
import QuickStart from '../modals/quick-start/QuickStart';
|
||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
@@ -29,7 +28,6 @@ export default function Editor() {
|
||||
} = useDisclosure();
|
||||
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
|
||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -44,7 +42,6 @@ export default function Editor() {
|
||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
|
||||
<SettingsModal isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<div id='settings' className={styles.settings}>
|
||||
@@ -61,8 +58,6 @@ export default function Editor() {
|
||||
onAboutOpen={onAboutModalOpen}
|
||||
isQuickStartOpen={isQuickStartOpen}
|
||||
onQuickStartOpen={onQuickStartOpen}
|
||||
isSheetsOpen={isSheetsOpen}
|
||||
onSheetsOpen={onSheetsOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -73,8 +73,8 @@ export default function EventEditor() {
|
||||
handleSubmit={handleSubmit}
|
||||
>
|
||||
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
||||
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue/${event.cue}`}</CopyTag>
|
||||
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid "${event.id}"`}</CopyTag>
|
||||
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue "${event.cue}"`}</CopyTag>
|
||||
</EventEditorDataRight>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
font-size: $inner-section-text-size;
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
@@ -14,12 +15,12 @@
|
||||
|
||||
.selected {
|
||||
min-width: max-content;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { IoCalendarOutline } from '@react-icons/all-files/io5/IoCalendarOutline';
|
||||
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||
@@ -32,8 +31,6 @@ interface MenuBarProps {
|
||||
onAboutOpen: () => void;
|
||||
isQuickStartOpen: boolean;
|
||||
onQuickStartOpen: () => void;
|
||||
isSheetsOpen: boolean;
|
||||
onSheetsOpen: () => void;
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
@@ -61,8 +58,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
onAboutOpen,
|
||||
isQuickStartOpen,
|
||||
onQuickStartOpen,
|
||||
isSheetsOpen,
|
||||
onSheetsOpen,
|
||||
} = props;
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
@@ -179,16 +174,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
/>
|
||||
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
isDisabled={appMode === AppMode.Run}
|
||||
icon={<IoCalendarOutline />}
|
||||
className={isSheetsOpen ? style.open : ''}
|
||||
clickHandler={onSheetsOpen}
|
||||
tooltip='Sheets'
|
||||
aria-label='Sheets'
|
||||
size='sm'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
isDisabled={appMode === AppMode.Run}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/r
|
||||
|
||||
import ModalWrapper from '../ModalWrapper';
|
||||
|
||||
import OscIntegration from './OscIntegration';
|
||||
import OscSettings from './OscSettings';
|
||||
import HttpIntegration from './http/HttpIntegration';
|
||||
import OscIntegration from './osc/OscIntegration';
|
||||
import OscSettings from './osc/OscSettings';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
|
||||
@@ -30,6 +31,7 @@ export default function IntegrationModal(props: IntegrationModalProps) {
|
||||
<TabList>
|
||||
<Tab>OSC</Tab>
|
||||
<Tab>OSC Integration</Tab>
|
||||
<Tab>HTTP Integration</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
@@ -38,6 +40,9 @@ export default function IntegrationModal(props: IntegrationModalProps) {
|
||||
<TabPanel>
|
||||
<OscIntegration />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<HttpIntegration />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import type { HttpSettings } from 'ontime-types';
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
||||
import { useEmitLog } from '../../../../common/stores/logger';
|
||||
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||
import { OntimeCycle, sectionText } from '../integration.utils';
|
||||
|
||||
import HttpSubscriptionRow from './HttpSubscriptionRow';
|
||||
|
||||
import styles from '../../Modal.module.scss';
|
||||
|
||||
export default function HttpIntegration() {
|
||||
const { data, isFetching } = useHttpSettings();
|
||||
const { mutateAsync } = usePostHttpSettings();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty, isValid },
|
||||
} = useForm<HttpSettings>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [showSection, setShowSection] = useState<OntimeCycle>(TimerLifeCycle.onLoad);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const resetForm = () => {
|
||||
reset(data);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: HttpSettings) => {
|
||||
try {
|
||||
const newSettings: HttpSettings = {
|
||||
enabledOut: Boolean(values.enabledOut),
|
||||
subscriptions: {
|
||||
onLoad: values.subscriptions.onLoad ?? [],
|
||||
onStart: values.subscriptions.onStart ?? [],
|
||||
onPause: values.subscriptions.onPause ?? [],
|
||||
onStop: values.subscriptions.onStop ?? [],
|
||||
onUpdate: values.subscriptions.onUpdate ?? [],
|
||||
onFinish: values.subscriptions.onFinish ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
await mutateAsync(newSettings);
|
||||
} catch (error) {
|
||||
emitError(`Error setting HTML: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
const placeholder = 'http://x.x.x.x:xxxx/api/path';
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='http-subscriptions'>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>HTTP Output</span>
|
||||
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
|
||||
</div>
|
||||
<Switch {...register('enabledOut')} variant='ontime-on-light' />
|
||||
</div>
|
||||
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onLoad}
|
||||
title={sectionText.onLoad.title}
|
||||
subtitle={sectionText.onLoad.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onLoad}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStart}
|
||||
title={sectionText.onStart.title}
|
||||
subtitle={sectionText.onStart.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onStart}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onPause}
|
||||
title={sectionText.onPause.title}
|
||||
subtitle={sectionText.onPause.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onPause}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStop}
|
||||
title={sectionText.onStop.title}
|
||||
subtitle={sectionText.onStop.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onStop}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onUpdate}
|
||||
title={sectionText.onUpdate.title}
|
||||
subtitle={sectionText.onUpdate.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onUpdate}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<HttpSubscriptionRow
|
||||
cycle={TimerLifeCycle.onFinish}
|
||||
title={sectionText.onFinish.title}
|
||||
subtitle={sectionText.onFinish.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onFinish}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OntimeModalFooter
|
||||
formId='http-subscriptions'
|
||||
handleRevert={resetForm}
|
||||
isDirty={isDirty}
|
||||
isValid={isValid}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { HttpSettings, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { useEmitLog } from '../../../../common/stores/logger';
|
||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||
|
||||
import collapseStyles from '../../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||
import styles from '../../Modal.module.scss';
|
||||
|
||||
interface SubscriptionRowProps {
|
||||
cycle: TimerLifeCycle;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
visible: boolean;
|
||||
setShowSection: (cycle: TimerLifeCycle) => void;
|
||||
register: UseFormRegister<HttpSettings>;
|
||||
control: Control<HttpSettings>;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export default function SubscriptionRow(props: SubscriptionRowProps) {
|
||||
const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: `subscriptions.${cycle}`,
|
||||
control,
|
||||
});
|
||||
|
||||
const hasTooManyOptions = fields.length >= 3;
|
||||
const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`;
|
||||
|
||||
const sectionTitle = `${title} ${fields.length ? fields.length : '-'} / 3`;
|
||||
|
||||
const handleAddNew = () => {
|
||||
if (hasTooManyOptions) {
|
||||
emitError(`Maximum amount of ${cycle} subscriptions reached (3)`);
|
||||
return;
|
||||
}
|
||||
append({
|
||||
message: '',
|
||||
enabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyle} onClick={() => setShowSection(cycle)}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>{sectionTitle}</span>
|
||||
{visible && <span className={styles.sectionSubtitle}>{subtitle}</span>}
|
||||
</div>
|
||||
<FiChevronUp className={visible ? collapseStyles.moreCollapsed : collapseStyles.moreExpanded} />
|
||||
</div>
|
||||
{visible && (
|
||||
<>
|
||||
{fields.map((subscription, index) => (
|
||||
<div key={subscription.id} className={styles.entryRow}>
|
||||
<IconButton
|
||||
icon={<IoRemove />}
|
||||
onClick={() => remove(index)}
|
||||
aria-label='delete'
|
||||
size='xs'
|
||||
colorScheme='red'
|
||||
/>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
size='xs'
|
||||
variant='ontime-filled-on-light'
|
||||
autoComplete='off'
|
||||
{...register(`subscriptions.${cycle}.${index}.message`, {
|
||||
pattern: { value: startsWithHttp, message: 'Request address must start with http://' },
|
||||
})}
|
||||
/>
|
||||
<Switch variant='ontime-on-light' {...register(`subscriptions.${cycle}.${index}.enabled`)} />
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
onClick={handleAddNew}
|
||||
className={styles.shiftRight}
|
||||
isDisabled={hasTooManyOptions}
|
||||
size='xs'
|
||||
colorScheme='blue'
|
||||
variant='outline'
|
||||
padding='0 2em'
|
||||
>
|
||||
Add new
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
export type OntimeCycle = keyof typeof TimerLifeCycle;
|
||||
|
||||
export const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = {
|
||||
onLoad: {
|
||||
title: 'On Load',
|
||||
subtitle: 'Triggers when a timer is loaded',
|
||||
},
|
||||
onStart: {
|
||||
title: 'On Start',
|
||||
subtitle: 'Triggers when a timer starts',
|
||||
},
|
||||
onPause: {
|
||||
title: 'On Pause',
|
||||
subtitle: 'Triggers when a running timer is paused',
|
||||
},
|
||||
onStop: {
|
||||
title: 'On Stop',
|
||||
subtitle: 'Triggers when a running timer is stopped',
|
||||
},
|
||||
onUpdate: {
|
||||
title: 'On Every Second',
|
||||
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
||||
},
|
||||
onFinish: {
|
||||
title: 'On Finish',
|
||||
subtitle: 'Triggers when a running reaches 0',
|
||||
},
|
||||
};
|
||||
+13
-34
@@ -3,43 +3,15 @@ import { useForm } from 'react-hook-form';
|
||||
import type { OscSubscription } from 'ontime-types';
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
import useOscSettings, { usePostOscSubscriptions } from '../../../../common/hooks-query/useOscSettings';
|
||||
import { useEmitLog } from '../../../../common/stores/logger';
|
||||
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||
import { type OntimeCycle, sectionText } from '../integration.utils';
|
||||
|
||||
import OscSubscriptionRow from './OscSubscriptionRow';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
|
||||
type OntimeCycle = keyof typeof TimerLifeCycle;
|
||||
|
||||
const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = {
|
||||
onLoad: {
|
||||
title: 'On Load',
|
||||
subtitle: 'Triggers when a timer is loaded',
|
||||
},
|
||||
onStart: {
|
||||
title: 'On Start',
|
||||
subtitle: 'Triggers when a timer starts',
|
||||
},
|
||||
onPause: {
|
||||
title: 'On Pause',
|
||||
subtitle: 'Triggers when a running timer is paused',
|
||||
},
|
||||
onStop: {
|
||||
title: 'On Stop',
|
||||
subtitle: 'Triggers when a running timer is stopped',
|
||||
},
|
||||
onUpdate: {
|
||||
title: 'On Every Second',
|
||||
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
||||
},
|
||||
onFinish: {
|
||||
title: 'On Finish',
|
||||
subtitle: 'Triggers when a running reaches 0',
|
||||
},
|
||||
};
|
||||
import styles from '../../Modal.module.scss';
|
||||
|
||||
export default function OscIntegration() {
|
||||
const { data, isFetching } = useOscSettings();
|
||||
@@ -92,6 +64,7 @@ export default function OscIntegration() {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
const placeholder = 'OSC message';
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='osc-subscriptions'>
|
||||
<OscSubscriptionRow
|
||||
@@ -102,6 +75,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStart}
|
||||
@@ -111,6 +85,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onPause}
|
||||
@@ -120,6 +95,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStop}
|
||||
@@ -129,6 +105,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onUpdate}
|
||||
@@ -138,6 +115,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onFinish}
|
||||
@@ -147,6 +125,7 @@ export default function OscIntegration() {
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<OntimeModalFooter
|
||||
formId='osc-subscriptions'
|
||||
+7
-7
@@ -4,14 +4,14 @@ import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
||||
|
||||
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
|
||||
import { PlaceholderSettings } from '../../../common/models/OscSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||
import { PlaceholderSettings } from '../../../../common/models/OscSettings';
|
||||
import { useEmitLog } from '../../../../common/stores/logger';
|
||||
import { isIPAddress, isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
import styles from '../../Modal.module.scss';
|
||||
|
||||
export default function OscSettings() {
|
||||
const { data, isFetching } = useOscSettings();
|
||||
+7
-6
@@ -2,12 +2,12 @@ import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { OscSubscription, TimerLifeCycle } from 'ontime-types';
|
||||
import type { OscSubscription, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { useEmitLog } from '../../../../common/stores/logger';
|
||||
|
||||
import collapseStyles from '../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||
import styles from '../Modal.module.scss';
|
||||
import collapseStyles from '../../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||
import styles from '../../Modal.module.scss';
|
||||
|
||||
interface OscSubscriptionRowProps {
|
||||
cycle: TimerLifeCycle;
|
||||
@@ -17,10 +17,11 @@ interface OscSubscriptionRowProps {
|
||||
setShowSection: (cycle: TimerLifeCycle) => void;
|
||||
register: UseFormRegister<OscSubscription>;
|
||||
control: Control<OscSubscription>;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
||||
const { cycle, title, subtitle, visible, setShowSection, register, control } = props;
|
||||
const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: cycle,
|
||||
@@ -64,7 +65,7 @@ export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
||||
colorScheme='red'
|
||||
/>
|
||||
<Input
|
||||
placeholder='OSC Message'
|
||||
placeholder={placeholder}
|
||||
size='xs'
|
||||
variant='ontime-filled-on-light'
|
||||
autoComplete='off'
|
||||
@@ -1,262 +0,0 @@
|
||||
import { ChangeEvent, useEffect,useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoArrowDownCircleOutline } from '@react-icons/all-files/io5/IoArrowDownCircleOutline';
|
||||
import { IoArrowUpCircleOutline } from '@react-icons/all-files/io5/IoArrowUpCircleOutline';
|
||||
import { IoCheckmarkCircleOutline } from '@react-icons/all-files/io5/IoCheckmarkCircleOutline';
|
||||
import { IoCloseCircleOutline } from '@react-icons/all-files/io5/IoCloseCircleOutline';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { GoogleSheetState, OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
||||
|
||||
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
||||
import { maybeAxiosError } from '../../../common/api/apiUtils';
|
||||
import {
|
||||
getSheetsAuthUrl,
|
||||
getSheetSettings,
|
||||
getSheetstate,
|
||||
patchData,
|
||||
postPreviewSheet,
|
||||
postPushSheet,
|
||||
postSheetSettings,
|
||||
uploadSheetClientFile,
|
||||
} from '../../../common/api/ontimeApi';
|
||||
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
||||
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
||||
import PreviewExcel from '../upload-modal/preview/PreviewExcel';
|
||||
|
||||
interface SheetsModalProps {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function SheetsModal(props: SheetsModalProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
||||
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
||||
const [project, setProject] = useState<ProjectData | null>(null);
|
||||
|
||||
const [sheetState, setSheetState] = useState<GoogleSheetState>({ auth: false, id: false, worksheet: false });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const sheetid = useRef<HTMLInputElement>(null);
|
||||
const worksheet = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setRundown(null);
|
||||
setProject(null);
|
||||
setUserFields(null);
|
||||
onClose();
|
||||
};
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFile = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event?.target?.files?.[0];
|
||||
if (!selectedFile) {
|
||||
return;
|
||||
} else {
|
||||
await uploadSheetClientFile(selectedFile);
|
||||
_onChange();
|
||||
}
|
||||
};
|
||||
|
||||
const _onChange = async () => {
|
||||
setSheetState(await getSheetstate());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSheetSettings().then((data) => {
|
||||
if (sheetid.current?.value != data.id || worksheet.current?.value != data.worksheet) {
|
||||
_onChange();
|
||||
}
|
||||
if (sheetid.current) {
|
||||
sheetid.current.value = data.id;
|
||||
}
|
||||
if (worksheet.current) {
|
||||
worksheet.current.value = data.worksheet;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handelSave = () => {
|
||||
postSheetSettings({ id: sheetid.current?.value ?? '', worksheet: worksheet.current?.value ?? '' }).then((data) => {
|
||||
_onChange();
|
||||
if (sheetid.current) {
|
||||
sheetid.current.value = data.id;
|
||||
}
|
||||
if (worksheet.current) {
|
||||
worksheet.current.value = data.worksheet;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAuthenticate = () => {
|
||||
getSheetsAuthUrl().then((data) => {
|
||||
if (data != 'bad') {
|
||||
window.open(data, '_blank', 'noreferrer');
|
||||
//TODO: can we detect when this window is closed
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePullData = () => {
|
||||
postPreviewSheet().then((data) => {
|
||||
setProject(data.project);
|
||||
setRundown(data.rundown);
|
||||
setUserFields(data.userFields);
|
||||
});
|
||||
};
|
||||
|
||||
const handlePushData = () => {
|
||||
postPushSheet().then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinalise = async () => {
|
||||
// this step is currently only used for excel files, after preview
|
||||
if (rundown && userFields && project) {
|
||||
let doClose = false;
|
||||
try {
|
||||
await patchData({ rundown, userFields, project });
|
||||
queryClient.setQueryData(RUNDOWN, rundown);
|
||||
queryClient.setQueryData(USERFIELDS, userFields);
|
||||
queryClient.setQueryData(PROJECT_DATA, project);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...RUNDOWN, ...USERFIELDS, ...PROJECT_DATA],
|
||||
});
|
||||
doClose = true;
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
console.log(message);
|
||||
// setErrors(`Failed applying changes ${message}`);
|
||||
} finally {
|
||||
if (doClose) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
preserveScrollBarGap
|
||||
variant='ontime-upload'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Sheets!</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{rundown && (
|
||||
<>
|
||||
<PreviewExcel
|
||||
rundown={rundown ?? []}
|
||||
project={project ?? projectDataPlaceholder}
|
||||
userFields={userFields ?? userFieldsPlaceholder}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!rundown && (
|
||||
<>
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
type='file'
|
||||
onChange={handleFile}
|
||||
accept='.json'
|
||||
data-testid='file-input'
|
||||
/>
|
||||
<div>Need to add some help here</div>
|
||||
<div>
|
||||
<Button onClick={handleClick}>Upload Client Secrect</Button>
|
||||
</div>
|
||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleAuthenticate}>
|
||||
Authenticate
|
||||
</Button>
|
||||
{sheetState.auth ? <div>You are authenticated</div> : <div>You are not authenticated</div>}
|
||||
<div>
|
||||
<label htmlFor='sheetid'>Sheet ID </label>
|
||||
<Input
|
||||
type='text'
|
||||
ref={sheetid}
|
||||
id='sheetid'
|
||||
width='240px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
variant='ontime-filled-on-light'
|
||||
/>
|
||||
{sheetState.id ? <IoCheckmarkCircleOutline /> : <IoCloseCircleOutline />}
|
||||
<br />
|
||||
<label htmlFor='worksheet'>Worksheet </label>
|
||||
<Input
|
||||
type='text'
|
||||
ref={worksheet}
|
||||
id='worksheet'
|
||||
width='240px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
variant='ontime-filled-on-light'
|
||||
/>
|
||||
{sheetState.worksheet ? <IoCheckmarkCircleOutline /> : <IoCloseCircleOutline />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{!rundown && (
|
||||
<div>
|
||||
<Button
|
||||
variant='ontime-subtle-on-light'
|
||||
padding='0 2em'
|
||||
onClick={handlePullData}
|
||||
rightIcon={<IoArrowDownCircleOutline />}
|
||||
>
|
||||
Pull data
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-subtle-on-light'
|
||||
padding='0 2em'
|
||||
onClick={handlePushData}
|
||||
rightIcon={<IoArrowUpCircleOutline />}
|
||||
>
|
||||
Push data
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button variant='ontime-ghost-on-light'>Reset</Button>
|
||||
{!rundown && (
|
||||
<Button variant='ontime-filled' padding='0 2em' onClick={handelSave}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
{rundown && (
|
||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useOperator } from '../../common/hooks/useSocket';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||
import { debounce } from '../../common/utils/debounce';
|
||||
import { isStringBoolean } from '../../common/utils/viewUtils';
|
||||
@@ -40,6 +41,7 @@ export default function Operator() {
|
||||
|
||||
const featureData = useOperator();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
||||
const [editEvent, setEditEvent] = useState<PartialEdit | null>(null);
|
||||
@@ -127,7 +129,7 @@ export default function Operator() {
|
||||
const subscribedAlias = subscribe ? userFields[subscribe] : '';
|
||||
const showSeconds = isStringBoolean(searchParams.get('showseconds'));
|
||||
|
||||
const operatorOptions = getOperatorOptions(userFields);
|
||||
const operatorOptions = getOperatorOptions(userFields, settings?.timeFormat ?? '24');
|
||||
let isPast = Boolean(featureData.selectedEventId);
|
||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ComponentType, useMemo } from 'react';
|
||||
import { TimeManagerType } from 'common/models/TimeManager.type';
|
||||
import { Message, OntimeEvent, ProjectData, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||
import { runtime } from '../../common/stores/runtime';
|
||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
@@ -27,6 +28,7 @@ type WithDataProps = {
|
||||
nextId: string | null;
|
||||
general: ProjectData;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
onAir: boolean;
|
||||
};
|
||||
|
||||
@@ -43,6 +45,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
const { data: rundownData } = useRundown();
|
||||
const { data: project } = useProjectData();
|
||||
const { data: viewSettings } = useViewSettings();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
const publicEvents = useMemo(() => {
|
||||
if (Array.isArray(rundownData)) {
|
||||
@@ -104,6 +107,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
selectedId={selectedId}
|
||||
publicSelectedId={publicSelectedId}
|
||||
viewSettings={viewSettings}
|
||||
settings={settings}
|
||||
nextId={nextId}
|
||||
general={project}
|
||||
onAir={onAir}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
@@ -11,7 +11,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { BACKSTAGE_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getBackstageOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -37,10 +37,12 @@ interface BackstageProps {
|
||||
selectedId: string | null;
|
||||
general: ProjectData;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function Backstage(props: BackstageProps) {
|
||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings, settings } =
|
||||
props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [blinkClass, setBlinkClass] = useState(false);
|
||||
@@ -89,11 +91,12 @@ export default function Backstage(props: BackstageProps) {
|
||||
}
|
||||
|
||||
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
||||
const backstageOptions = getBackstageOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={BACKSTAGE_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={backstageOptions} />
|
||||
<div className='project-header'>
|
||||
{general.title}
|
||||
<div className='clock-container'>
|
||||
@@ -137,7 +140,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
{isNegative ? (
|
||||
<div className='aux-timers__value'>{expectedFinish}</div>
|
||||
) : (
|
||||
<SuperscriptTime time={startedAt} className='aux-timers__value' />
|
||||
<SuperscriptTime time={expectedFinish} className='aux-timers__value' />
|
||||
)}
|
||||
</div>
|
||||
<div className='timer-gap' />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
import { Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { CLOCK_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -18,6 +18,7 @@ interface ClockProps {
|
||||
isMirrored: boolean;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
const formatOptions = {
|
||||
@@ -26,7 +27,7 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Clock(props: ClockProps) {
|
||||
const { isMirrored, time, viewSettings } = props;
|
||||
const { isMirrored, time, viewSettings, settings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -124,6 +125,8 @@ export default function Clock(props: ClockProps) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const clean = clock.replace('/:/g', '');
|
||||
|
||||
const clockOptions = getClockOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
|
||||
@@ -135,7 +138,7 @@ export default function Clock(props: ClockProps) {
|
||||
data-testid='clock-view'
|
||||
>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={CLOCK_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={clockOptions} />
|
||||
<SuperscriptTime
|
||||
time={clock}
|
||||
className='clock'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
|
||||
import { getTimeOption } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -34,10 +34,11 @@ interface CountdownProps {
|
||||
time: TimeManagerType;
|
||||
selectedId: string | null;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function Countdown(props: CountdownProps) {
|
||||
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
|
||||
const { isMirrored, backstageEvents, time, selectedId, viewSettings, settings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const { getLocalizedString } = useTranslation();
|
||||
@@ -109,10 +110,12 @@ export default function Countdown(props: CountdownProps) {
|
||||
isSelected || runningMessage === TimerMessage.waiting,
|
||||
);
|
||||
|
||||
const timeOption = getTimeOption(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
|
||||
<ViewParamsEditor paramFields={[timeOption]} />
|
||||
{follow === null ? (
|
||||
<CountdownSelect events={backstageEvents} />
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, ViewSettings } from 'ontime-types';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
@@ -9,7 +9,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { PUBLIC_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getPublicOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -35,11 +35,22 @@ interface BackstageProps {
|
||||
publicSelectedId: string | null;
|
||||
general: ProjectData;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function Public(props: BackstageProps) {
|
||||
const { isMirrored, publ, publicEventNow, publicEventNext, time, events, publicSelectedId, general, viewSettings } =
|
||||
props;
|
||||
const {
|
||||
isMirrored,
|
||||
publ,
|
||||
publicEventNow,
|
||||
publicEventNext,
|
||||
time,
|
||||
events,
|
||||
publicSelectedId,
|
||||
general,
|
||||
viewSettings,
|
||||
settings,
|
||||
} = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
@@ -56,10 +67,12 @@ export default function Public(props: BackstageProps) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||
|
||||
const publicOptions = getPublicOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={PUBLIC_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={publicOptions} />
|
||||
<div className='project-header'>
|
||||
{general.title}
|
||||
<div className='clock-container'>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { OntimeEvent, OntimeRundown, ViewSettings } from 'ontime-types';
|
||||
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { STUDIO_CLOCK_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -32,10 +32,11 @@ interface StudioClockProps {
|
||||
nextId: string | null;
|
||||
onAir: boolean;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function StudioClock(props: StudioClockProps) {
|
||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
||||
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
@@ -75,10 +76,12 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
const secondsNow = secondsInMillis(time.clock);
|
||||
const isNegative = (time.current ?? 0) < 0;
|
||||
|
||||
const studioClockOptions = getStudioClockOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={STUDIO_CLOCK_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||
<div className='clock-container'>
|
||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||
import { Message, OntimeEvent, Playback, Settings, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import { getTimerOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -48,10 +48,11 @@ interface TimerProps {
|
||||
eventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
settings: Settings | undefined;
|
||||
}
|
||||
|
||||
export default function Timer(props: TimerProps) {
|
||||
const { isMirrored, pres, eventNow, eventNext, time, viewSettings, external } = props;
|
||||
const { isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -128,10 +129,12 @@ export default function Timer(props: TimerProps) {
|
||||
const timerContainerClasses = `timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`;
|
||||
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
|
||||
|
||||
const timerOptions = getTimerOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={TIMER_OPTIONS} />
|
||||
<ViewParamsEditor paramFields={timerOptions} />
|
||||
{!userOptions.hideMessage && (
|
||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "2.16.2",
|
||||
"version": "2.24.8",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "2.16.2",
|
||||
"version": "2.24.8",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
"body-parser": "^1.20.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
@@ -13,7 +12,7 @@
|
||||
"express-session": "^1.17.3",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"express-validator": "^6.14.2",
|
||||
"google-auth-library": "^9.2.0",
|
||||
"got": "^14.0.0",
|
||||
"lowdb": "^5.0.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-osc": "^9.0.2",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LogOrigin, OSCSettings } from 'ontime-types';
|
||||
import { Server } from 'node-osc';
|
||||
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||
import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
|
||||
export class OscServer implements IAdapter {
|
||||
@@ -36,12 +36,36 @@ export class OscServer implements IAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
let transformedPayload: unknown = args;
|
||||
// we need to transform the params for the change endpoint
|
||||
// OSC: ontime/change/{eventID}/{propertyName} value
|
||||
if (path === 'change') {
|
||||
if (params.length < 2) {
|
||||
logger.error(LogOrigin.Rx, 'OSC IN: No params provided for change');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args === undefined) {
|
||||
logger.error(LogOrigin.Rx, 'OSC IN: No valid payload provided for change');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = params[0];
|
||||
const property = params[1];
|
||||
const value: string | number | boolean = args as string | number | boolean;
|
||||
|
||||
transformedPayload = {
|
||||
eventId,
|
||||
property,
|
||||
value,
|
||||
} satisfies ChangeOptions;
|
||||
}
|
||||
|
||||
try {
|
||||
const reply = dispatchFromAdapter(
|
||||
path,
|
||||
{
|
||||
payload: args,
|
||||
params,
|
||||
payload: transformedPayload,
|
||||
},
|
||||
'osc',
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import type { Server } from 'http';
|
||||
|
||||
import getRandomName from '../utils/getRandomName.js';
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
@@ -43,7 +44,7 @@ export class SocketServer implements IAdapter {
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
init(server) {
|
||||
init(server: Server) {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
|
||||
+22
-12
@@ -1,9 +1,9 @@
|
||||
import { LogOrigin, OSCSettings } from 'ontime-types';
|
||||
import { HttpSettings, LogOrigin, OSCSettings } from 'ontime-types';
|
||||
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import expressStaticGzip from 'express-static-gzip';
|
||||
import http from 'http';
|
||||
import http, { type Server } from 'http';
|
||||
import cors from 'cors';
|
||||
|
||||
// import utils
|
||||
@@ -29,6 +29,7 @@ import { eventLoader } from './classes/event-loader/EventLoader.js';
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { logger } from './classes/Logger.js';
|
||||
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
||||
import { httpIntegration } from './services/integration-service/HttpIntegration.js';
|
||||
import { populateStyles } from './modules/loadStyles.js';
|
||||
import { eventStore, getInitialPayload } from './stores/EventStore.js';
|
||||
import { PlaybackService } from './services/PlaybackService.js';
|
||||
@@ -107,8 +108,8 @@ enum OntimeStartOrder {
|
||||
}
|
||||
|
||||
let step = OntimeStartOrder.InitAssets;
|
||||
let expressServer = null;
|
||||
let oscServer = null;
|
||||
let expressServer: Server | null = null;
|
||||
let oscServer: OscServer | null = null;
|
||||
|
||||
const checkStart = (currentState: OntimeStartOrder) => {
|
||||
if (step !== currentState) {
|
||||
@@ -165,7 +166,6 @@ export const startServer = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description starts OSC server
|
||||
* @description starts OSC server
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
@@ -194,20 +194,30 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
/**
|
||||
* starts integrations
|
||||
*/
|
||||
export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
export const startIntegrations = async (config?: { osc: OSCSettings; http: HttpSettings }) => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
const { osc } = config ?? DataProvider.getData();
|
||||
const { osc, http } = config ?? DataProvider.getData();
|
||||
|
||||
if (!osc) {
|
||||
return 'OSC Invalid configuration';
|
||||
} else {
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
logger.info(LogOrigin.Tx, message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
}
|
||||
}
|
||||
if (!http) {
|
||||
return 'HTTP Invalid configuration';
|
||||
} else {
|
||||
const { success, message } = httpIntegration.init(http);
|
||||
logger.info(LogOrigin.Tx, message);
|
||||
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
logger.info(LogOrigin.Tx, message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
if (success) {
|
||||
integrationService.register(httpIntegration);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
UserFields,
|
||||
Alias,
|
||||
Settings,
|
||||
GoogleSheet,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
@@ -59,19 +58,14 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getGoogleSheet() {
|
||||
return data.googleSheet;
|
||||
}
|
||||
|
||||
static async setGoogleSheet(newData: GoogleSheet) {
|
||||
data.googleSheet = { ...newData };
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getOsc() {
|
||||
return data.osc;
|
||||
}
|
||||
|
||||
static getHttp() {
|
||||
return data.http;
|
||||
}
|
||||
|
||||
static getAliases() {
|
||||
return data.aliases;
|
||||
}
|
||||
@@ -104,6 +98,11 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static async setHttp(newData) {
|
||||
data.http = { ...newData };
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getRundown() {
|
||||
return [...data.rundown];
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import { DatabaseModel } from 'ontime-types';
|
||||
* @param {object} newData
|
||||
*/
|
||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
||||
const { rundown, project, settings, googleSheet, viewSettings, osc, aliases, userFields } = newData || {};
|
||||
const { rundown, project, settings, viewSettings, osc, aliases, userFields } = newData || {};
|
||||
return {
|
||||
...existing,
|
||||
rundown: rundown ?? existing.rundown,
|
||||
project: { ...existing.project, ...project },
|
||||
settings: { ...existing.settings, ...settings },
|
||||
googleSheet: { ...existing.googleSheet, ...googleSheet },
|
||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||
aliases: aliases ?? existing.aliases,
|
||||
userFields: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alias, DatabaseModel, GoogleSheet, OntimeRundown, Settings } from 'ontime-types';
|
||||
import { Alias, DatabaseModel, OntimeRundown, Settings } from 'ontime-types';
|
||||
import { safeMerge } from '../DataProvider.utils.js';
|
||||
|
||||
describe('safeMerge', () => {
|
||||
@@ -21,10 +21,6 @@ describe('safeMerge', () => {
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
},
|
||||
googleSheet: {
|
||||
worksheet: '1',
|
||||
id: '2',
|
||||
},
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
endMessage: 'existing endMessage',
|
||||
@@ -102,20 +98,6 @@ describe('safeMerge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the google sheet key', () => {
|
||||
const newData = {
|
||||
googleSheet: {
|
||||
id: '4',
|
||||
worksheet: '5',
|
||||
} as GoogleSheet,
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.googleSheet).toEqual({
|
||||
id: '4',
|
||||
worksheet: '5',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the osc key', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LogOrigin, OntimeEvent } from 'ontime-types';
|
||||
import { EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { editEvent } from '../services/rundown-service/RundownService.js';
|
||||
import { coerceString, coerceNumber, coerceBoolean } from '../utils/coerceType.js';
|
||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { isKeyOfType, isOntimeEvent } from 'ontime-types/src/utils/guards.js';
|
||||
|
||||
const whitelistedPayload = {
|
||||
title: coerceString,
|
||||
@@ -16,7 +17,8 @@ const whitelistedPayload = {
|
||||
isPublic: coerceBoolean,
|
||||
skip: coerceBoolean,
|
||||
|
||||
colour: coerceString,
|
||||
colour: coerceColour,
|
||||
|
||||
user0: coerceString,
|
||||
user1: coerceString,
|
||||
user2: coerceString,
|
||||
@@ -29,12 +31,12 @@ const whitelistedPayload = {
|
||||
user9: coerceString,
|
||||
};
|
||||
|
||||
export function parse(field: string, value: unknown) {
|
||||
if (!Object.hasOwn(whitelistedPayload, field)) {
|
||||
throw new Error(`Field ${field} not permitted`);
|
||||
export function parse(property: string, value: unknown) {
|
||||
if (!isKeyOfType(property, whitelistedPayload)) {
|
||||
throw new Error(`Property ${property} not permitted`);
|
||||
}
|
||||
const parserFn = whitelistedPayload[field];
|
||||
return parserFn(value);
|
||||
const parserFn = whitelistedPayload[property];
|
||||
return { parsedProperty: property, parsedPayload: parserFn(value) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +51,10 @@ export function updateEvent(
|
||||
newValue: OntimeEvent[typeof propertyName],
|
||||
) {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
|
||||
if (event) {
|
||||
if (!isOntimeEvent(event)) {
|
||||
throw new Error(`Can only update events`);
|
||||
}
|
||||
const propertiesToUpdate = { [propertyName]: newValue };
|
||||
|
||||
// Handles the special case for duration
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { messageService } from '../services/message-service/MessageService.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { parse, updateEvent } from './integrationController.config.js';
|
||||
import { isKeyOfType } from 'ontime-types/src/utils/guards.js';
|
||||
import { event } from '../models/eventsDefinition.js';
|
||||
|
||||
export type ChangeOptions = {
|
||||
eventId: string;
|
||||
property: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
//TODO: re-throwing the error does not add any extra information or value
|
||||
export function dispatchFromAdapter(
|
||||
type: string,
|
||||
args: {
|
||||
payload: unknown;
|
||||
params?: Array<string>;
|
||||
},
|
||||
_source?: 'osc' | 'ws',
|
||||
) {
|
||||
const payload = args.payload;
|
||||
const typeComponents = type.toLowerCase().split('/');
|
||||
const mainType = typeComponents[0];
|
||||
const params = args.params || [];
|
||||
|
||||
switch (mainType) {
|
||||
case 'test-ontime': {
|
||||
@@ -267,21 +267,11 @@ export function dispatchFromAdapter(
|
||||
return { topic: 'timer', payload: timer };
|
||||
}
|
||||
|
||||
// ontime/change/{eventID}/{propertyName}
|
||||
// WS: {type: 'change', payload: { eventId, property, value } }
|
||||
case 'change': {
|
||||
if (params.length < 2) {
|
||||
throw new Error('Too few parameters, 3 expected');
|
||||
}
|
||||
if (payload === undefined) {
|
||||
throw new Error('No payload found');
|
||||
}
|
||||
const eventID = params[0];
|
||||
const propertyName = params[1] as keyof OntimeEvent;
|
||||
if (!isKeyOfType(propertyName, event)) {
|
||||
throw new Error(`Cannot update unknown event property ${propertyName}`);
|
||||
}
|
||||
const parsedPayload = parse(propertyName, payload);
|
||||
return updateEvent(eventID, propertyName, parsedPayload);
|
||||
const { eventId, property, value } = payload as ChangeOptions;
|
||||
const { parsedPayload, parsedProperty } = parse(property, value);
|
||||
return updateEvent(eventId, parsedProperty, parsedPayload);
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alias, DatabaseModel, GetInfo, LogOrigin, ProjectData } from 'ontime-types';
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
import type { Alias, DatabaseModel, GetInfo, HttpSettings, ProjectData } from 'ontime-types';
|
||||
|
||||
import { RequestHandler, Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
@@ -11,13 +12,13 @@ import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { isDocker, pathToStartStyles, resolveDbPath } from '../setup.js';
|
||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { deleteAllEvents, notifyChanges } from '../services/rundown-service/RundownService.js';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
import { runtimeCacheStore } from '../stores/cachingStore.js';
|
||||
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
||||
|
||||
import { Sheet } from '../utils/sheetsAuth.js';
|
||||
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
@@ -285,15 +286,42 @@ export const getOSC = async (req, res) => {
|
||||
res.status(200).send(osc);
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/osc'
|
||||
// Returns ACK message
|
||||
export const postOSC = async (req, res) => {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oscSettings = req.body;
|
||||
await DataProvider.setOsc(oscSettings);
|
||||
|
||||
integrationService.unregister(oscIntegration);
|
||||
|
||||
// TODO: this update could be more granular, checking that relevant data was changed
|
||||
const { success, message } = oscIntegration.init(oscSettings);
|
||||
logger.info(LogOrigin.Tx, message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
}
|
||||
|
||||
res.send(oscSettings).status(200);
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
export const postOscSubscriptions = async (req, res) => {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oscSubscriptions = req.body;
|
||||
const subscriptions = req.body;
|
||||
const oscSettings = DataProvider.getOsc();
|
||||
oscSettings.subscriptions = oscSubscriptions;
|
||||
oscSettings.subscriptions = subscriptions;
|
||||
await DataProvider.setOsc(oscSettings);
|
||||
|
||||
// TODO: this update could be more granular, checking that relevant data was changed
|
||||
@@ -306,22 +334,33 @@ export const postOscSubscriptions = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/osc'
|
||||
// Returns ACK message
|
||||
export const postOSC = async (req, res) => {
|
||||
// Create controller for GET request to '/ontime/http'
|
||||
export const getHTTP = async (_req, res: Response<HttpSettings>) => {
|
||||
const http = DataProvider.getHttp();
|
||||
res.status(200).send(http);
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/http'
|
||||
export const postHTTP = async (req, res) => {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oscSettings = req.body;
|
||||
await DataProvider.setOsc(oscSettings);
|
||||
const httpSettings = req.body;
|
||||
await DataProvider.setHttp(httpSettings);
|
||||
|
||||
integrationService.unregister(httpIntegration);
|
||||
|
||||
// TODO: this update could be more granular, checking that relevant data was changed
|
||||
const { message } = oscIntegration.init(oscSettings);
|
||||
const { success, message } = httpIntegration.init(httpSettings);
|
||||
logger.info(LogOrigin.Tx, message);
|
||||
|
||||
res.send(oscSettings).status(200);
|
||||
if (success) {
|
||||
integrationService.register(httpIntegration);
|
||||
}
|
||||
|
||||
res.send(httpSettings).status(200);
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
@@ -416,101 +455,3 @@ export const postNew: RequestHandler = async (req, res) => {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* downloads and parses an sheet
|
||||
* @returns parsed result
|
||||
*/
|
||||
export async function previewSheet(req, res) {
|
||||
try {
|
||||
const data = await Sheet.pull();
|
||||
res.status(200).send(data);
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* downloads and parses an sheet
|
||||
* @returns parsed result
|
||||
*/
|
||||
export async function pushSheet(req, res) {
|
||||
try {
|
||||
await Sheet.push();
|
||||
res.status(200).send('ok');
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads Client secrets file
|
||||
* @returns parsed result
|
||||
*/
|
||||
export async function uploadGoogleSheetClientFile(req, res) {
|
||||
if (!req.file.path) {
|
||||
res.status(400).send({ message: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = JSON.parse(fs.readFileSync(req.file.path as string, 'utf-8'));
|
||||
await Sheet.saveClientSecrets(client);
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
fs.unlink(req.file.path, (err) => {
|
||||
if (err) logger.error(LogOrigin.Server, err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns link to sheet auth url
|
||||
*/
|
||||
export async function sheetAuthUrl(req, res) {
|
||||
const successful = await Sheet.openAuthServer();
|
||||
if (successful === false) {
|
||||
res.status(500).send('bad');
|
||||
} else {
|
||||
res.status(200).send(successful);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Get google sheet Settings
|
||||
* @method GET
|
||||
*/
|
||||
export const getGoogleSheetSettings = async (req, res) => {
|
||||
const sheet = DataProvider.getGoogleSheet();
|
||||
res.status(200).send(sheet);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Change view Settings
|
||||
* @method POST
|
||||
*/
|
||||
export const postGoogleSheetSettings = async (req, res) => {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newData = {
|
||||
id: req.body.id,
|
||||
worksheet: req.body.worksheet,
|
||||
};
|
||||
await DataProvider.setGoogleSheet(newData);
|
||||
res.status(200).send(newData);
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Get google sheet state
|
||||
* @method GET
|
||||
*/
|
||||
export const getGoogleSheetState = async (req, res) => {
|
||||
res.status(200).send(await Sheet.getSheetState());
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { body, check, validationResult } from 'express-validator';
|
||||
import { validateOscObject, validateOscSubscriptionEntry } from '../utils/parserFunctions.js';
|
||||
import {
|
||||
validateHttpSubscriptionObject,
|
||||
validateOscSubscriptionObject,
|
||||
validateOscSubscriptionCycle,
|
||||
} from '../utils/parserFunctions.js';
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/views
|
||||
@@ -82,7 +86,22 @@ export const validateOSC = [
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateOscObject(value)),
|
||||
.custom((value) => validateOscSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/http
|
||||
*/
|
||||
export const validateHTTP = [
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateHttpSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
@@ -96,22 +115,22 @@ export const validateOSC = [
|
||||
export const validateOscSubscription = [
|
||||
body('onLoad')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
body('onStart')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
body('onPause')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
body('onStop')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
body('onUpdate')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
body('onFinish')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionEntry(value)),
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
@@ -133,25 +152,3 @@ export const validatePatchProjectFile = [
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
//TODO: is thise correct
|
||||
export const validateSheetPreview = [
|
||||
body('sheetid').isString().optional({ nullable: false }),
|
||||
body('worksheet').isString().optional({ nullable: false }),
|
||||
body('options').isObject().optional({ nullable: true }),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
export const validateGoogleSheetSettings = [
|
||||
body('id').isString().optional({ nullable: false }),
|
||||
body('worksheet').isString().optional({ nullable: false }),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
@@ -20,10 +20,6 @@ export const dbModel: DatabaseModel = {
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
},
|
||||
googleSheet: {
|
||||
worksheet: '',
|
||||
id: '',
|
||||
},
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
@@ -61,4 +57,15 @@ export const dbModel: DatabaseModel = {
|
||||
onFinish: [],
|
||||
},
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: [],
|
||||
onStart: [],
|
||||
onPause: [],
|
||||
onStop: [],
|
||||
onUpdate: [],
|
||||
onFinish: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getAliases,
|
||||
getInfo,
|
||||
getOSC,
|
||||
getHTTP,
|
||||
getSettings,
|
||||
getUserFields,
|
||||
getViewSettings,
|
||||
@@ -19,25 +20,18 @@ import {
|
||||
postUserFields,
|
||||
postViewSettings,
|
||||
previewExcel,
|
||||
sheetAuthUrl,
|
||||
uploadGoogleSheetClientFile,
|
||||
previewSheet,
|
||||
pushSheet,
|
||||
getGoogleSheetSettings,
|
||||
postGoogleSheetSettings,
|
||||
getGoogleSheetState,
|
||||
postHTTP,
|
||||
} from '../controllers/ontimeController.js';
|
||||
|
||||
import {
|
||||
validateAliases,
|
||||
validateGoogleSheetSettings,
|
||||
validateOSC,
|
||||
validateOscSubscription,
|
||||
validatePatchProjectFile,
|
||||
validateSettings,
|
||||
validateSheetPreview,
|
||||
validateUserFields,
|
||||
viewValidator,
|
||||
validateHTTP,
|
||||
validateOscSubscription,
|
||||
} from '../controllers/ontimeController.validate.js';
|
||||
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
||||
|
||||
@@ -94,26 +88,11 @@ router.post('/osc', validateOSC, postOSC);
|
||||
// create route between controller and '/ontime/osc-subscriptions' endpoint
|
||||
router.post('/osc-subscriptions', validateOscSubscription, postOscSubscriptions);
|
||||
|
||||
// create route between controller and '/ontime/http' endpoint
|
||||
router.get('/http', getHTTP);
|
||||
|
||||
// create route between controller and '/ontime/http' endpoint
|
||||
router.post('/http', validateHTTP, postHTTP);
|
||||
|
||||
// create route between controller and '/ontime/new' endpoint
|
||||
router.post('/new', projectSanitiser, postNew);
|
||||
|
||||
// create route between controller and '/ontime/sheet-client' endpoint
|
||||
router.post('/sheet-clientsecrect', uploadFile, uploadGoogleSheetClientFile);
|
||||
|
||||
// create route between controller and '/ontime/sheet-authstatus' endpoint
|
||||
router.get('/sheet-authurl', sheetAuthUrl);
|
||||
|
||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
||||
router.post('/sheet-preview', validateSheetPreview, previewSheet);
|
||||
|
||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
||||
router.post('/sheet-push', pushSheet);
|
||||
|
||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
||||
router.get('/sheet-settings', getGoogleSheetSettings);
|
||||
|
||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
||||
router.post('/sheet-settings', validateGoogleSheetSettings, postGoogleSheetSettings);
|
||||
|
||||
// create route between controller and '/ontime/sheet-state' endpoint
|
||||
router.get('/sheet-state', getGoogleSheetState);
|
||||
@@ -5,7 +5,7 @@ import { eventStore } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
import { updateRoll } from './rollUtils.js';
|
||||
import { integrationService } from './integration-service/IntegrationService.js';
|
||||
import { getCurrent, getExpectedFinish } from './timerUtils.js';
|
||||
import { getCurrent, getExpectedFinish, skippedOutOfEvent } from './timerUtils.js';
|
||||
import { clock } from './Clock.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import type { RestorePoint } from './RestoreService.js';
|
||||
@@ -18,10 +18,13 @@ type initialLoadingData = {
|
||||
|
||||
type RestoreCallback = (newState: RestorePoint) => Promise<void>;
|
||||
|
||||
export const timeSkipLimit = 3 * 32;
|
||||
|
||||
export class TimerService {
|
||||
private readonly _interval: NodeJS.Timer;
|
||||
private _updateInterval: number;
|
||||
private _lastUpdate: number | null;
|
||||
private _skipThreshold: number;
|
||||
|
||||
playback: Playback;
|
||||
timer: TimerState;
|
||||
@@ -40,11 +43,13 @@ export class TimerService {
|
||||
* @param {object} [timerConfig]
|
||||
* @param {number} [timerConfig.refresh]
|
||||
* @param {number} [timerConfig.updateInterval]
|
||||
* @param {number} [timerConfig.skipThreshold]
|
||||
*/
|
||||
constructor(timerConfig: { refresh?: number; updateInterval?: number } = {}) {
|
||||
constructor(timerConfig: { refresh: number; updateInterval: number; skipThreshold: number }) {
|
||||
this._clear();
|
||||
this._interval = setInterval(() => this.update(), timerConfig?.refresh ?? 1000);
|
||||
this._updateInterval = timerConfig?.updateInterval ?? 1000;
|
||||
this._interval = setInterval(() => this.update(), timerConfig.refresh);
|
||||
this._updateInterval = timerConfig.updateInterval;
|
||||
this._skipThreshold = timerConfig.skipThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,7 +344,6 @@ export class TimerService {
|
||||
this.timer.expectedFinish >= this.timer.startedAt
|
||||
? this.timer.expectedFinish
|
||||
: this.timer.expectedFinish + dayInMs,
|
||||
|
||||
clock: this.timer.clock,
|
||||
secondaryTimer: this.timer.secondaryTimer,
|
||||
secondaryTarget: this.secondaryTarget,
|
||||
@@ -405,7 +409,19 @@ export class TimerService {
|
||||
let shouldNotify = false;
|
||||
if (this.playback === Playback.Roll) {
|
||||
shouldNotify = true;
|
||||
this.updateRoll();
|
||||
if (
|
||||
skippedOutOfEvent(
|
||||
previousTime,
|
||||
this.timer.clock,
|
||||
this.timer.startedAt,
|
||||
this.timer.expectedFinish,
|
||||
this._skipThreshold,
|
||||
)
|
||||
) {
|
||||
PlaybackService.roll();
|
||||
} else {
|
||||
this.updateRoll();
|
||||
}
|
||||
} else if (this.timer.startedAt !== null) {
|
||||
// we only update timer if a timer has been started
|
||||
shouldNotify = true;
|
||||
@@ -505,4 +521,5 @@ export class TimerService {
|
||||
}
|
||||
|
||||
// calculate at 30fps, refresh at 1fps
|
||||
export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000 });
|
||||
// we consider a skip at 3 lost updates
|
||||
export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000, skipThreshold: 32 * 3 });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
import { TimerType } from 'ontime-types';
|
||||
|
||||
import { getCurrent, getExpectedFinish } from '../timerUtils.js';
|
||||
import { getCurrent, getExpectedFinish, skippedOutOfEvent } from '../timerUtils.js';
|
||||
|
||||
describe('getExpectedFinish()', () => {
|
||||
it('is null if we havent started', () => {
|
||||
@@ -354,3 +354,106 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
||||
expect(current).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('skippedOutOfEvent()', () => {
|
||||
const testSkipLimit = 32;
|
||||
it('does not consider an event end as a skip', () => {
|
||||
const startedAt = 1000;
|
||||
const duration = 1000;
|
||||
const expectedFinish = startedAt + duration;
|
||||
const previousTime = expectedFinish - testSkipLimit / 2;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock += testSkipLimit;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows rolling backwards in an event', () => {
|
||||
const startedAt = 1000;
|
||||
const duration = 1000;
|
||||
const expectedFinish = startedAt + duration;
|
||||
const previousTime = startedAt + testSkipLimit / 2;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock -= testSkipLimit;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
});
|
||||
|
||||
it('accounts for crossing midnight', () => {
|
||||
const startedAt = dayInMs - testSkipLimit;
|
||||
const expectedFinish = 10;
|
||||
const previousTime = dayInMs - 1;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock = testSkipLimit - 2;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows rolling backwards in an event across midnight', () => {
|
||||
const startedAt = dayInMs - testSkipLimit;
|
||||
const expectedFinish = 10;
|
||||
const previousTime = startedAt + 1;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock -= testSkipLimit;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
});
|
||||
|
||||
it('finds skip forwards out of event', () => {
|
||||
const startedAt = 1000;
|
||||
const duration = 1000;
|
||||
const expectedFinish = startedAt + duration;
|
||||
const previousTime = expectedFinish - testSkipLimit / 2;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock += testSkipLimit + 1;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true);
|
||||
});
|
||||
|
||||
it('finds skip backwards out of event', () => {
|
||||
const startedAt = 1000;
|
||||
const duration = 1000;
|
||||
const expectedFinish = startedAt + duration;
|
||||
const previousTime = startedAt + testSkipLimit / 2;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock -= testSkipLimit + 1;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true);
|
||||
});
|
||||
|
||||
it('finds skip forwards out of event across midnight', () => {
|
||||
const startedAt = dayInMs - testSkipLimit;
|
||||
const expectedFinish = 10;
|
||||
const previousTime = dayInMs - 3;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock = testSkipLimit - 2;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true);
|
||||
});
|
||||
|
||||
it('finds skip backwards out of event across midnight', () => {
|
||||
const startedAt = dayInMs - testSkipLimit;
|
||||
const expectedFinish = 10;
|
||||
const previousTime = startedAt + 1;
|
||||
|
||||
let clock = previousTime;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false);
|
||||
|
||||
clock -= testSkipLimit + 1;
|
||||
expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import got from 'got';
|
||||
|
||||
import { HttpSettings, HttpSubscription, HttpSubscriptionOptions, LogOrigin } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { validateHttpSubscriptionObject } from '../../utils/parserFunctions.js';
|
||||
|
||||
type Action = TimerLifeCycleKey | string;
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing HTTP communications
|
||||
* @class
|
||||
*/
|
||||
export class HttpIntegration implements IIntegration<HttpSubscriptionOptions> {
|
||||
subscriptions: HttpSubscription;
|
||||
constructor() {
|
||||
this.subscriptions = dbModel.http.subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes httpClient
|
||||
*/
|
||||
init(config: HttpSettings) {
|
||||
const { subscriptions, enabledOut } = config;
|
||||
|
||||
if (!enabledOut) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'HTTP output disabled',
|
||||
};
|
||||
}
|
||||
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: `HTTP integration client ready`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed initialising HTTP integration: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptionOptions: HttpSubscription) {
|
||||
if (validateHttpSubscriptionObject(subscriptionOptions)) {
|
||||
this.subscriptions = { ...subscriptionOptions };
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action: Action, state?: object) {
|
||||
if (!action) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'HTTP called with no action',
|
||||
};
|
||||
}
|
||||
|
||||
// check subscriptions for action
|
||||
const eventSubscriptions = this.subscriptions?.[action] || [];
|
||||
|
||||
eventSubscriptions.forEach((sub) => {
|
||||
const { enabled, message } = sub;
|
||||
if (enabled && message) {
|
||||
const parsedMessage = parseTemplateNested(message, state || {});
|
||||
try {
|
||||
const parsedUrl = new URL(parsedMessage);
|
||||
this.emit(parsedUrl);
|
||||
} catch (err) {
|
||||
logger.error(LogOrigin.Tx, `HTTP Integration: ${err}`);
|
||||
return {
|
||||
success: false,
|
||||
message: `${err}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async emit(path: URL) {
|
||||
try {
|
||||
await got.get(path, {
|
||||
retry: { limit: 0 },
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(LogOrigin.Tx, `HTTP integration: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {}
|
||||
}
|
||||
|
||||
export const httpIntegration = new HttpIntegration();
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TimerLifeCycle, OscSubscription } from 'ontime-types';
|
||||
import { TimerLifeCycle, Subscription } from 'ontime-types';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
export default interface IIntegration {
|
||||
subscriptions: OscSubscription;
|
||||
export default interface IIntegration<T> {
|
||||
subscriptions: Subscription<T>;
|
||||
init: (config: unknown) => OperationReturn;
|
||||
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
|
||||
emit: (...args: unknown[]) => unknown;
|
||||
|
||||
@@ -2,17 +2,17 @@ import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
class IntegrationService {
|
||||
private integrations: IIntegration[];
|
||||
private integrations: IIntegration<unknown>[];
|
||||
|
||||
constructor() {
|
||||
this.integrations = [];
|
||||
}
|
||||
|
||||
register(integrationService: IIntegration) {
|
||||
register(integrationService: IIntegration<unknown>) {
|
||||
this.integrations.push(integrationService);
|
||||
}
|
||||
|
||||
unregister(integrationService: IIntegration) {
|
||||
unregister(integrationService: IIntegration<unknown>) {
|
||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArgumentType, Client, Message } from 'node-osc';
|
||||
import { OSCSettings, OscSubscription } from 'ontime-types';
|
||||
import { OSCSettings, OscSubscription, OscSubscriptionOptions } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { isObject } from '../../utils/varUtils.js';
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { validateOscObject } from '../../utils/parserFunctions.js';
|
||||
import { validateOscSubscriptionObject } from '../../utils/parserFunctions.js';
|
||||
|
||||
type Action = TimerLifeCycleKey | string;
|
||||
|
||||
@@ -13,7 +13,7 @@ type Action = TimerLifeCycleKey | string;
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OscIntegration implements IIntegration {
|
||||
export class OscIntegration implements IIntegration<OscSubscriptionOptions> {
|
||||
protected oscClient: null | Client;
|
||||
subscriptions: OscSubscription;
|
||||
|
||||
@@ -26,7 +26,15 @@ export class OscIntegration implements IIntegration {
|
||||
* Initializes oscClient
|
||||
*/
|
||||
init(config: OSCSettings) {
|
||||
const { targetIP, portOut, subscriptions } = config;
|
||||
const { targetIP, portOut, subscriptions, enabledOut } = config;
|
||||
|
||||
if (!enabledOut) {
|
||||
this.oscClient?.close();
|
||||
return {
|
||||
success: false,
|
||||
message: 'OSC output disabled',
|
||||
};
|
||||
}
|
||||
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
@@ -58,7 +66,7 @@ export class OscIntegration implements IIntegration {
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptionOptions: OscSubscription) {
|
||||
if (validateOscObject(subscriptionOptions)) {
|
||||
if (validateOscSubscriptionObject(subscriptionOptions)) {
|
||||
this.subscriptions = { ...subscriptionOptions };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,3 +64,20 @@ export function getCurrent(
|
||||
}
|
||||
return startedAt + duration + addedTime + pausedTime - clock;
|
||||
}
|
||||
|
||||
export function skippedOutOfEvent(
|
||||
previousTime: number,
|
||||
clock: number,
|
||||
startedAt: number,
|
||||
expectedFinish: number,
|
||||
skipLimit: number,
|
||||
): boolean {
|
||||
const hasPassedMidnight = previousTime > dayInMs - skipLimit && clock < skipLimit;
|
||||
const adjustedClock = hasPassedMidnight ? clock + dayInMs : clock;
|
||||
|
||||
const timeDifference = previousTime - adjustedClock;
|
||||
const hasSkipped = Math.abs(timeDifference) > skipLimit;
|
||||
const adjustedExpectedFinish = expectedFinish >= startedAt ? expectedFinish : expectedFinish + dayInMs;
|
||||
|
||||
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { coerceColour } from '../coerceType.js';
|
||||
|
||||
describe('parses a colour string that is', () => {
|
||||
it('valid hex', () => {
|
||||
const color = coerceColour('#000');
|
||||
expect(color).toBe('#000');
|
||||
});
|
||||
it('valid name', () => {
|
||||
const color = coerceColour('darkgoldenrod');
|
||||
expect(color).toBe('darkgoldenrod');
|
||||
});
|
||||
it('invalid hex', () => {
|
||||
expect(() => coerceColour('#not a hex color')).toThrowError(Error('Invalid hex colour received'));
|
||||
});
|
||||
it('invalid name', () => {
|
||||
expect(() => coerceColour('bad name')).toThrowError(Error('Invalid colour name received'));
|
||||
});
|
||||
it('not a string', () => {
|
||||
expect(() => coerceColour(5)).toThrowError(Error('Invalid colour value received'));
|
||||
});
|
||||
});
|
||||
@@ -1,433 +0,0 @@
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import { getA1Notation, cellRequenstFromEvent, cellRequenstFromProjectData } from '../googleSheetUtils.js';
|
||||
import { EndAction, OntimeRundownEntry, ProjectData, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
describe('getA1Notation()', () => {
|
||||
test('A1', () => {
|
||||
expect(getA1Notation(0, 0)).toStrictEqual('A1');
|
||||
});
|
||||
test('E3', () => {
|
||||
expect(getA1Notation(2, 4)).toStrictEqual('E3');
|
||||
});
|
||||
test('AA100', () => {
|
||||
expect(getA1Notation(99, 26)).toStrictEqual('AA100');
|
||||
});
|
||||
test('can not be negative', () => {
|
||||
expect(() => getA1Notation(-1, 1)).toThrowError('Index can not be less than 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellRequenstFromEvent()', () => {
|
||||
test('string to string', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
|
||||
});
|
||||
|
||||
test('numer to timer', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue
|
||||
.stringValue;
|
||||
expect(result).toStrictEqual(millisToString(event.duration));
|
||||
});
|
||||
|
||||
test('boolean to x', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[11].userEnteredValue.stringValue).toStrictEqual('x');
|
||||
expect(result.updateCells.rows[0].values[12].userEnteredValue.stringValue).toStrictEqual('');
|
||||
});
|
||||
|
||||
test('spacing in metadata', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 0 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[6].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells.rows[0].values[10].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 5 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[1].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 10, col: 5 },
|
||||
title: { row: 10, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 10, col: 16 },
|
||||
};
|
||||
const result1 = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||
const result2 = cellRequenstFromEvent(event, 10, 1234, metadata);
|
||||
expect(result2.updateCells.start.rowIndex).toStrictEqual(21);
|
||||
expect(result2.updateCells.start.columnIndex).toStrictEqual(5);
|
||||
expect(result2.updateCells.fields).toStrictEqual('userEnteredValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellRequenstFromProjectData()', () => {
|
||||
test('string to string', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 1 },
|
||||
description: { row: 1, col: 1 },
|
||||
publicUrl: { row: 2, col: 1 },
|
||||
backstageUrl: { row: 3, col: 1 },
|
||||
publicInfo: { row: 4, col: 1 },
|
||||
backstageInfo: { row: 5, col: 1 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[3].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[4].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[5].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 5, col: 10 },
|
||||
description: { row: 6, col: 10 },
|
||||
publicUrl: { row: 7, col: 10 },
|
||||
backstageUrl: { row: 9, col: 10 },
|
||||
publicInfo: { row: 10, col: 10 },
|
||||
backstageInfo: { row: 11, col: 10 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[4].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[5].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[6].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('spacing in metadata', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 1 },
|
||||
description: { row: 1, col: 1 },
|
||||
publicUrl: { row: 2, col: 1 },
|
||||
backstageUrl: { row: 9, col: 1 },
|
||||
publicInfo: { row: 15, col: 1 },
|
||||
backstageInfo: { row: 50, col: 1 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[9].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[15].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[50].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 10 },
|
||||
description: { row: 1, col: 10 },
|
||||
publicUrl: { row: 2, col: 10 },
|
||||
backstageUrl: { row: 3, col: 10 },
|
||||
publicInfo: { row: 4, col: 10 },
|
||||
backstageInfo: { row: 5, col: 10 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.start.rowIndex).toStrictEqual(0);
|
||||
expect(result.updateCells.start.columnIndex).toStrictEqual(11);
|
||||
expect(result.updateCells.fields).toStrictEqual('userEnteredValue');
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import { validateOscObject } from '../parserFunctions.ts';
|
||||
|
||||
test('validateOscSubscription()', () => {
|
||||
it('should return true when given a valid OscSubscription', () => {
|
||||
const validSubscription = {
|
||||
onLoad: [{ id: '1', message: 'test', enabled: true }],
|
||||
onStart: [{ id: '2', message: 'test', enabled: false }],
|
||||
onPause: [{ id: '3', message: 'test', enabled: true }],
|
||||
onStop: [{ id: '4', message: 'test', enabled: false }],
|
||||
onUpdate: [{ id: '5', message: 'test', enabled: true }],
|
||||
onFinish: [{ id: '6', message: 'test', enabled: false }],
|
||||
};
|
||||
|
||||
const result = validateOscObject(validSubscription);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when given undefined', () => {
|
||||
const result = validateOscObject(undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given null', () => {
|
||||
const result = validateOscObject(null);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty object', () => {
|
||||
const result = validateOscObject({});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty array', () => {
|
||||
const result = validateOscObject([]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an object that is not an OscSubscription', () => {
|
||||
const invalidObject = { foo: 'bar' };
|
||||
|
||||
const result = validateOscObject(invalidObject);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an OscSubscription with a missing property', () => {
|
||||
const invalidSubscription = {
|
||||
onLoad: [{ id: '1', message: 'test', enabled: true }],
|
||||
onStart: [{ id: '2', message: 'test', enabled: false }],
|
||||
onPause: [{ id: '3', message: 'test', enabled: true }],
|
||||
// Missing onStop
|
||||
onUpdate: [{ id: '5', message: 'test', enabled: true }],
|
||||
onFinish: [{ id: '6', message: 'test', enabled: false }],
|
||||
};
|
||||
|
||||
const result = validateOscObject(invalidSubscription);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an OscSubscription with an invalid property value', () => {
|
||||
const invalidSubscription = {
|
||||
onLoad: [{ id: '1', message: 'test', enabled: true }],
|
||||
onStart: [{ id: '2', message: 'test', enabled: false }],
|
||||
onPause: [{ id: '3', message: 'test', enabled: true }],
|
||||
onStop: [{ id: '4', message: 'test', enabled: false }],
|
||||
onUpdate: [{ id: '5', message: 'test', enabled: true }],
|
||||
onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }],
|
||||
};
|
||||
|
||||
const result = validateOscObject(invalidSubscription);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the message field is empty', () => {
|
||||
const invalidSubscription = {
|
||||
onLoad: [{ id: '1', message: 'test', enabled: true }],
|
||||
onStart: [{ id: '2', message: '', enabled: false }],
|
||||
onPause: [{ id: '3', message: '', enabled: true }],
|
||||
onStop: [{ id: '4', message: 'test', enabled: false }],
|
||||
onUpdate: [{ id: '5', message: 'test', enabled: true }],
|
||||
onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }],
|
||||
};
|
||||
|
||||
const result = validateOscObject(invalidSubscription);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { HttpSubscription, OscSubscription } from 'ontime-types';
|
||||
import {
|
||||
validateOscSubscriptionObject,
|
||||
validateOscSubscriptionCycle,
|
||||
validateHttpSubscriptionCycle,
|
||||
validateHttpSubscriptionObject,
|
||||
} from '../parserFunctions.js';
|
||||
|
||||
describe('validateOscSubscriptionCycle()', () => {
|
||||
it('should return false when given an OscSubscription with an invalid property value', () => {
|
||||
const invalidEntry = [{ message: 'test', enabled: 'not a boolean' }];
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionCycle(invalidEntry);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOscSubscriptionObject()', () => {
|
||||
it('should return true when given a valid OscSubscription', () => {
|
||||
const validSubscription: OscSubscription = {
|
||||
onLoad: [{ message: 'test', enabled: true }],
|
||||
onStart: [{ message: 'test', enabled: false }],
|
||||
onPause: [{ message: 'test', enabled: true }],
|
||||
onStop: [{ message: 'test', enabled: false }],
|
||||
onUpdate: [{ message: 'test', enabled: true }],
|
||||
onFinish: [{ message: 'test', enabled: false }],
|
||||
};
|
||||
|
||||
const result = validateOscSubscriptionObject(validSubscription);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when given undefined', () => {
|
||||
const result = validateOscSubscriptionObject(undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given null', () => {
|
||||
const result = validateOscSubscriptionObject(null);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty object', () => {
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionObject({});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty array', () => {
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionObject([]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an object that is not an OscSubscription', () => {
|
||||
const invalidObject = { foo: 'bar' };
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionObject(invalidObject);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an OscSubscription with a missing property', () => {
|
||||
const invalidSubscription = {
|
||||
onLoad: [{ message: 'test', enabled: true }],
|
||||
onStart: [{ message: 'test', enabled: false }],
|
||||
onPause: [{ message: 'test', enabled: true }],
|
||||
// Missing onStop
|
||||
onUpdate: [{ message: 'test', enabled: true }],
|
||||
onFinish: [{ message: 'test', enabled: false }],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionObject(invalidSubscription);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateHttpSubscriptionCycle()', () => {
|
||||
it('should return false when given an HttpSubscription with an invalid property value', () => {
|
||||
const invalidBoolean = [{ message: 'http://', enabled: 'not a boolean' }];
|
||||
const invalidHttp = [{ message: 'test', enabled: true }];
|
||||
const noFtp = [{ message: 'ftp://test', enabled: true }];
|
||||
const noEmpty = [{ message: '', enabled: true }];
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
expect(validateHttpSubscriptionCycle(invalidBoolean)).toBe(false);
|
||||
|
||||
expect(validateHttpSubscriptionCycle(invalidHttp)).toBe(false);
|
||||
expect(validateHttpSubscriptionCycle(noFtp)).toBe(false);
|
||||
expect(validateHttpSubscriptionCycle(noEmpty)).toBe(false);
|
||||
});
|
||||
it('should return true when given an HttpSubscription matches definition', () => {
|
||||
const validHttp = [{ message: 'http://', enabled: true }];
|
||||
const invalidHttps = [{ message: 'https://', enabled: true }];
|
||||
|
||||
expect(validateHttpSubscriptionCycle(validHttp)).toBe(true);
|
||||
expect(validateHttpSubscriptionCycle(invalidHttps)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateHttpSubscriptionObject()', () => {
|
||||
it('should return true when given a valid HttpSubscription', () => {
|
||||
const validSubscription: HttpSubscription = {
|
||||
onLoad: [{ message: 'http://', enabled: true }],
|
||||
onStart: [{ message: 'http://', enabled: false }],
|
||||
onPause: [{ message: 'http://', enabled: true }],
|
||||
onStop: [{ message: 'http://', enabled: false }],
|
||||
onUpdate: [{ message: 'http://', enabled: true }],
|
||||
onFinish: [{ message: 'http://', enabled: false }],
|
||||
};
|
||||
|
||||
const result = validateHttpSubscriptionObject(validSubscription);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when given undefined', () => {
|
||||
const result = validateHttpSubscriptionObject(undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given null', () => {
|
||||
const result = validateHttpSubscriptionObject(null);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty object', () => {
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateOscSubscriptionObject({});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an empty array', () => {
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateHttpSubscriptionObject([]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an object that is not an HttpSubscription', () => {
|
||||
const invalidObject = { foo: 'bar' };
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateHttpSubscriptionObject(invalidObject);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when given an HttpSubscription with a missing property', () => {
|
||||
const invalidSubscription = {
|
||||
onLoad: [{ message: 'http://', enabled: true }],
|
||||
onStart: [{ message: 'http://', enabled: false }],
|
||||
onPause: [{ message: 'http://', enabled: true }],
|
||||
// Missing onStop
|
||||
onUpdate: [{ message: 'http://', enabled: true }],
|
||||
onFinish: [{ message: 'http://', enabled: false }],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
|
||||
const result = validateHttpSubscriptionObject(invalidSubscription);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { isColourHex } from 'ontime-utils';
|
||||
|
||||
//TODO: write tests
|
||||
/**
|
||||
* @description Converts a value to a number if possible, throws otherwise
|
||||
* @description Converts a value to a string if possible, throws otherwise
|
||||
* @param {unknown} value - Value to be converted to a string.
|
||||
* @returns {string} - The converted value as a string.
|
||||
* @throws {Error} Throws an error if the value is null or undefined.
|
||||
@@ -11,8 +14,9 @@ export function coerceString(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
//TODO: write tests
|
||||
/**
|
||||
* @description Converts a value to a number if possible, throws otherwise
|
||||
* @description Converts a value to a boolean if possible, throws otherwise
|
||||
* @param {unknown} value - Value to be converted to a boolean.
|
||||
* @returns {boolean} - The converted value as a boolean.
|
||||
* @throws {Error} Throws an error if the value is null or undefined.
|
||||
@@ -21,9 +25,26 @@ export function coerceBoolean(value: unknown): boolean {
|
||||
if (value == null) {
|
||||
throw new Error('Invalid value received');
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const lowerCaseValue = value.toLocaleLowerCase();
|
||||
switch (lowerCaseValue) {
|
||||
case 'true':
|
||||
case '1':
|
||||
case 'yes':
|
||||
return true;
|
||||
case 'false':
|
||||
case '0':
|
||||
case 'no':
|
||||
case '':
|
||||
return false;
|
||||
default:
|
||||
throw new Error('Invalid value received');
|
||||
}
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
//TODO: write tests
|
||||
/**
|
||||
* @description Converts a value to a number if possible, throws otherwise
|
||||
* @param {unknown} value - Value to be converted to a number.
|
||||
@@ -40,3 +61,176 @@ export function coerceNumber(value: unknown): number {
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Converts a value to a colour if possible, throws otherwise
|
||||
* @param {unknown} value - Value to be converted to a colour.
|
||||
* @returns {string} - The converted value as a string.
|
||||
* @throws {Error} Throws an error if the value is null or undefined.
|
||||
*/
|
||||
export function coerceColour(value: unknown): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Invalid colour value received');
|
||||
}
|
||||
const lowerCaseValue = value.toLocaleLowerCase();
|
||||
if (lowerCaseValue.startsWith('#')) {
|
||||
if (!isColourHex(lowerCaseValue)) {
|
||||
throw new Error('Invalid hex colour received');
|
||||
}
|
||||
} else if (!(lowerCaseValue in cssColours)) {
|
||||
throw new Error('Invalid colour name received');
|
||||
}
|
||||
return lowerCaseValue;
|
||||
}
|
||||
|
||||
//https://developer.mozilla.org/en-US/docs/Web/CSS/named-color
|
||||
const cssColours = {
|
||||
aliceblue: '#f0f8ff',
|
||||
antiquewhite: '#faebd7',
|
||||
aqua: '#00ffff',
|
||||
aquamarine: '#7fffd4',
|
||||
azure: '#f0ffff',
|
||||
beige: '#f5f5dc',
|
||||
bisque: '#ffe4c4',
|
||||
black: '#000000',
|
||||
blanchedalmond: '#ffebcd',
|
||||
blue: '#0000ff',
|
||||
blueviolet: '#8a2be2',
|
||||
brown: '#a52a2a',
|
||||
burlywood: '#deb887',
|
||||
cadetblue: '#5f9ea0',
|
||||
chartreuse: '#7fff00',
|
||||
chocolate: '#d2691e',
|
||||
coral: '#ff7f50',
|
||||
cornflowerblue: '#6495ed',
|
||||
cornsilk: '#fff8dc',
|
||||
crimson: '#dc143c',
|
||||
cyan: '#00ffff',
|
||||
darkblue: '#00008b',
|
||||
darkcyan: '#008b8b',
|
||||
darkgoldenrod: '#b8860b',
|
||||
darkgray: '#a9a9a9',
|
||||
darkgreen: '#006400',
|
||||
darkgrey: '#a9a9a9',
|
||||
darkkhaki: '#bdb76b',
|
||||
darkmagenta: '#8b008b',
|
||||
darkolivegreen: '#556b2f',
|
||||
darkorange: '#ff8c00',
|
||||
darkorchid: '#9932cc',
|
||||
darkred: '#8b0000',
|
||||
darksalmon: '#e9967a',
|
||||
darkseagreen: '#8fbc8f',
|
||||
darkslateblue: '#483d8b',
|
||||
darkslategray: '#2f4f4f',
|
||||
darkslategrey: '#2f4f4f',
|
||||
darkturquoise: '#00ced1',
|
||||
darkviolet: '#9400d3',
|
||||
deeppink: '#ff1493',
|
||||
deepskyblue: '#00bfff',
|
||||
dimgray: '#696969',
|
||||
dimgrey: '#696969',
|
||||
dodgerblue: '#1e90ff',
|
||||
firebrick: '#b22222',
|
||||
floralwhite: '#fffaf0',
|
||||
forestgreen: '#228b22',
|
||||
fuchsia: '#ff00ff',
|
||||
gainsboro: '#dcdcdc',
|
||||
ghostwhite: '#f8f8ff',
|
||||
goldenrod: '#daa520',
|
||||
gold: '#ffd700',
|
||||
gray: '#808080',
|
||||
green: '#008000',
|
||||
greenyellow: '#adff2f',
|
||||
grey: '#808080',
|
||||
honeydew: '#f0fff0',
|
||||
hotpink: '#ff69b4',
|
||||
indianred: '#cd5c5c',
|
||||
indigo: '#4b0082',
|
||||
ivory: '#fffff0',
|
||||
khaki: '#f0e68c',
|
||||
lavenderblush: '#fff0f5',
|
||||
lavender: '#e6e6fa',
|
||||
lawngreen: '#7cfc00',
|
||||
lemonchiffon: '#fffacd',
|
||||
lightblue: '#add8e6',
|
||||
lightcoral: '#f08080',
|
||||
lightcyan: '#e0ffff',
|
||||
lightgoldenrodyellow: '#fafad2',
|
||||
lightgray: '#d3d3d3',
|
||||
lightgreen: '#90ee90',
|
||||
lightgrey: '#d3d3d3',
|
||||
lightpink: '#ffb6c1',
|
||||
lightsalmon: '#ffa07a',
|
||||
lightseagreen: '#20b2aa',
|
||||
lightskyblue: '#87cefa',
|
||||
lightslategray: '#778899',
|
||||
lightslategrey: '#778899',
|
||||
lightsteelblue: '#b0c4de',
|
||||
lightyellow: '#ffffe0',
|
||||
lime: '#00ff00',
|
||||
limegreen: '#32cd32',
|
||||
linen: '#faf0e6',
|
||||
magenta: '#ff00ff',
|
||||
maroon: '#800000',
|
||||
mediumaquamarine: '#66cdaa',
|
||||
mediumblue: '#0000cd',
|
||||
mediumorchid: '#ba55d3',
|
||||
mediumpurple: '#9370db',
|
||||
mediumseagreen: '#3cb371',
|
||||
mediumslateblue: '#7b68ee',
|
||||
mediumspringgreen: '#00fa9a',
|
||||
mediumturquoise: '#48d1cc',
|
||||
mediumvioletred: '#c71585',
|
||||
midnightblue: '#191970',
|
||||
mintcream: '#f5fffa',
|
||||
mistyrose: '#ffe4e1',
|
||||
moccasin: '#ffe4b5',
|
||||
navajowhite: '#ffdead',
|
||||
navy: '#000080',
|
||||
oldlace: '#fdf5e6',
|
||||
olive: '#808000',
|
||||
olivedrab: '#6b8e23',
|
||||
orange: '#ffa500',
|
||||
orangered: '#ff4500',
|
||||
orchid: '#da70d6',
|
||||
palegoldenrod: '#eee8aa',
|
||||
palegreen: '#98fb98',
|
||||
paleturquoise: '#afeeee',
|
||||
palevioletred: '#db7093',
|
||||
papayawhip: '#ffefd5',
|
||||
peachpuff: '#ffdab9',
|
||||
peru: '#cd853f',
|
||||
pink: '#ffc0cb',
|
||||
plum: '#dda0dd',
|
||||
powderblue: '#b0e0e6',
|
||||
purple: '#800080',
|
||||
rebeccapurple: '#663399',
|
||||
red: '#ff0000',
|
||||
rosybrown: '#bc8f8f',
|
||||
royalblue: '#4169e1',
|
||||
saddlebrown: '#8b4513',
|
||||
salmon: '#fa8072',
|
||||
sandybrown: '#f4a460',
|
||||
seagreen: '#2e8b57',
|
||||
seashell: '#fff5ee',
|
||||
sienna: '#a0522d',
|
||||
silver: '#c0c0c0',
|
||||
skyblue: '#87ceeb',
|
||||
slateblue: '#6a5acd',
|
||||
slategray: '#708090',
|
||||
slategrey: '#708090',
|
||||
snow: '#fffafa',
|
||||
springgreen: '#00ff7f',
|
||||
steelblue: '#4682b4',
|
||||
tan: '#d2b48c',
|
||||
teal: '#008080',
|
||||
thistle: '#d8bfd8',
|
||||
tomato: '#ff6347',
|
||||
turquoise: '#40e0d0',
|
||||
violet: '#ee82ee',
|
||||
wheat: '#f5deb3',
|
||||
white: '#ffffff',
|
||||
whitesmoke: '#f5f5f5',
|
||||
yellow: '#ffff00',
|
||||
yellowgreen: '#9acd3',
|
||||
} as const;
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { sheets_v4 } from '@googleapis/sheets';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import { OntimeRundownEntry, ProjectData, isOntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} row - The row number of the cell reference. Row 1 is row number 0.
|
||||
* @param {number} column - The column number of the cell reference. A is column number 0.
|
||||
* @returns {string} - Returns a cell reference as a string using A1 Notation
|
||||
* @author https://www.labnol.org/convert-column-a1-notation-210601
|
||||
* @example
|
||||
*
|
||||
* getA1Notation(2, 4) returns "E3"
|
||||
* getA1Notation(99, 26) returns "AA100"
|
||||
*
|
||||
*/
|
||||
export function getA1Notation(row: number, column: number): string {
|
||||
if (row < 0 || column < 0) {
|
||||
throw new Error('Index can not be less than 0');
|
||||
}
|
||||
const a1Notation = [`${row + 1}`];
|
||||
const totalAlphabets = 'Z'.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
|
||||
let block = column;
|
||||
while (block >= 0) {
|
||||
a1Notation.unshift(String.fromCharCode((block % totalAlphabets) + 'A'.charCodeAt(0)));
|
||||
block = Math.floor(block / totalAlphabets) - 1;
|
||||
}
|
||||
return a1Notation.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - creates updateCells request from ontime event
|
||||
* @param {OntimeRundownEntry} event
|
||||
* @param {number} index - index of the event
|
||||
* @param {number} worksheetId
|
||||
* @param {any} metadata - object with all the cell positions of the title of each attribute
|
||||
* @returns {sheets_v4.Schema} - list of update requests
|
||||
*/
|
||||
export function cellRequenstFromEvent(
|
||||
event: OntimeRundownEntry,
|
||||
index: number,
|
||||
worksheetId: number,
|
||||
metadata,
|
||||
): sheets_v4.Schema$Request {
|
||||
const r: sheets_v4.Schema$CellData[] = [];
|
||||
const tmp = Object.entries(metadata)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.sort(([_a, a], [_b, b]) => a['col'] - b['col']) as [string, { col: number; row: number }][];
|
||||
|
||||
const titleCol = tmp[0][1].col;
|
||||
|
||||
for (const [index, e] of tmp.entries()) {
|
||||
if (index != 0) {
|
||||
const prevCol = tmp[index - 1][1].col;
|
||||
const thisCol = e[1].col;
|
||||
const diff = thisCol - prevCol;
|
||||
if (diff > 1) {
|
||||
const fillArr = new Array<(typeof tmp)[0]>(1).fill(['blank', { row: e[1].row, col: prevCol + 1 }]);
|
||||
tmp.splice(index, 0, ...fillArr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp.forEach(([key, _]) => {
|
||||
if (isOntimeEvent(event)) {
|
||||
if (key === 'blank') {
|
||||
r.push({});
|
||||
} else if (key === 'colour') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event.colour },
|
||||
});
|
||||
} else if (typeof event[key] === 'number') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: millisToString(event[key], true) },
|
||||
});
|
||||
} else if (typeof event[key] === 'string') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event[key] },
|
||||
});
|
||||
} else if (typeof event[key] === 'boolean') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event[key] ? 'x' : '' },
|
||||
});
|
||||
} else {
|
||||
r.push({});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: index + tmp[0][1]['row'] + 1,
|
||||
columnIndex: titleCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: [
|
||||
{
|
||||
values: r,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - creates updateCells request from ontime event
|
||||
* @param {ProjectData} projectData
|
||||
* @param {number} worksheetId
|
||||
* @param {any} metadata - object with all the cell positions of the title of each attribute
|
||||
* @returns {sheets_v4.Schema} - list of update requests
|
||||
*/
|
||||
export function cellRequenstFromProjectData(
|
||||
projectData: ProjectData,
|
||||
worksheetId: number,
|
||||
metadata,
|
||||
): sheets_v4.Schema$Request {
|
||||
const r: sheets_v4.Schema$RowData[] = [];
|
||||
const tmp = Object.entries(metadata)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.sort(([_a, a], [_b, b]) => a['col'] - b['col']) as [string, { col: number; row: number }][];
|
||||
|
||||
const minRow = Object.values(metadata).reduce(
|
||||
(accumulator: number, val) => Math.min(accumulator, val['row']),
|
||||
Number.MAX_VALUE,
|
||||
) as number;
|
||||
const minCol = tmp[0][1].col + 1;
|
||||
|
||||
for (const [index, e] of tmp.entries()) {
|
||||
if (index != 0) {
|
||||
const prevRow = tmp[index - 1][1].row;
|
||||
const thisRow = e[1].row;
|
||||
const diff = thisRow - prevRow;
|
||||
if (diff > 1) {
|
||||
const fillArr = new Array<(typeof tmp)[0]>(1).fill(['blank', { row: prevRow + 1, col: e[1].col }]);
|
||||
tmp.splice(index, 0, ...fillArr);
|
||||
}
|
||||
}
|
||||
}
|
||||
tmp.forEach(([key, _]) => {
|
||||
if (key == 'blank') {
|
||||
r.push({});
|
||||
} else {
|
||||
r.push({
|
||||
values: [
|
||||
{
|
||||
userEnteredValue: { stringValue: projectData[key] },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: minRow,
|
||||
columnIndex: minCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: r,
|
||||
},
|
||||
};
|
||||
}
|
||||
+124
-218
@@ -29,11 +29,11 @@ import {
|
||||
parseAliases,
|
||||
parseProject,
|
||||
parseOsc,
|
||||
parseHttp,
|
||||
parseRundown,
|
||||
parseSettings,
|
||||
parseUserFields,
|
||||
parseViewSettings,
|
||||
parseGoogleSheet,
|
||||
} from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
|
||||
@@ -47,8 +47,6 @@ export const JSON_MIME = 'application/json';
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
|
||||
const projectMetadata = {};
|
||||
const rundownMetadata = {};
|
||||
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
||||
const projectData: Partial<ProjectData> = {
|
||||
title: '',
|
||||
@@ -105,228 +103,140 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
let user8Index: number | null = null;
|
||||
let user9Index: number | null = null;
|
||||
|
||||
excelData.forEach((row, rowIndex) => {
|
||||
if (row.length === 0) {
|
||||
return;
|
||||
}
|
||||
// these fields contain the data to its right
|
||||
let projectTitleNext = false;
|
||||
let projectDescriptionNext = false;
|
||||
let publicUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageUrlNext = false;
|
||||
let backstageInfoNext = false;
|
||||
excelData
|
||||
.filter((e) => e.length > 0)
|
||||
.forEach((row) => {
|
||||
// these fields contain the data to its right
|
||||
let projectTitleNext = false;
|
||||
let projectDescriptionNext = false;
|
||||
let publicUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageUrlNext = false;
|
||||
let backstageInfoNext = false;
|
||||
|
||||
const event: Partial<OntimeEvent> = {};
|
||||
const handlers = {
|
||||
[importMap.projectName]: (row: number, col: number) => {
|
||||
projectTitleNext = true;
|
||||
projectMetadata['title'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.projectDescription]: (row: number, col: number) => {
|
||||
projectDescriptionNext = true;
|
||||
projectMetadata['description'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.publicUrl]: (row: number, col: number) => {
|
||||
publicUrlNext = true;
|
||||
projectMetadata['publicUrl'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.publicInfo]: (row: number, col: number) => {
|
||||
publicInfoNext = true;
|
||||
projectMetadata['publicInfo'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.backstageUrl]: (row: number, col: number) => {
|
||||
backstageUrlNext = true;
|
||||
projectMetadata['backstageUrl'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.backstageInfo]: (row: number, col: number) => {
|
||||
backstageInfoNext = true;
|
||||
projectMetadata['backstageInfo'] = { row: row, col: col };
|
||||
},
|
||||
const event: Partial<OntimeEvent> = {};
|
||||
const handlers = {
|
||||
[importMap.projectName]: () => (projectTitleNext = true),
|
||||
[importMap.projectDescription]: () => (projectDescriptionNext = true),
|
||||
[importMap.publicUrl]: () => (publicUrlNext = true),
|
||||
[importMap.publicInfo]: () => (publicInfoNext = true),
|
||||
[importMap.backstageUrl]: () => (backstageUrlNext = true),
|
||||
[importMap.backstageInfo]: () => (backstageInfoNext = true),
|
||||
|
||||
[importMap.timeStart]: (row: number, col: number) => {
|
||||
timeStartIndex = col;
|
||||
rundownMetadata['timeStart'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.timeEnd]: (row: number, col: number) => {
|
||||
timeEndIndex = col;
|
||||
rundownMetadata['timeEnd'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.duration]: (row: number, col: number) => {
|
||||
durationIndex = col;
|
||||
rundownMetadata['duration'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.timeStart]: (index: number) => (timeStartIndex = index),
|
||||
[importMap.timeEnd]: (index: number) => (timeEndIndex = index),
|
||||
[importMap.duration]: (index: number) => (durationIndex = index),
|
||||
|
||||
[importMap.cue]: (row: number, col: number) => {
|
||||
cueIndex = col;
|
||||
rundownMetadata['cue'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.title]: (row: number, col: number) => {
|
||||
titleIndex = col;
|
||||
rundownMetadata['title'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.presenter]: (row: number, col: number) => {
|
||||
presenterIndex = col;
|
||||
rundownMetadata['presenter'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.subtitle]: (row: number, col: number) => {
|
||||
subtitleIndex = col;
|
||||
rundownMetadata['subtitle'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.isPublic]: (row: number, col: number) => {
|
||||
isPublicIndex = col;
|
||||
rundownMetadata['isPublic'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.skip]: (row: number, col: number) => {
|
||||
skipIndex = col;
|
||||
rundownMetadata['skip'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.note]: (row: number, col: number) => {
|
||||
notesIndex = col;
|
||||
rundownMetadata['note'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.colour]: (row: number, col: number) => {
|
||||
colourIndex = col;
|
||||
rundownMetadata['colour'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.cue]: (index: number) => (cueIndex = index),
|
||||
[importMap.title]: (index: number) => (titleIndex = index),
|
||||
[importMap.presenter]: (index: number) => (presenterIndex = index),
|
||||
[importMap.subtitle]: (index: number) => (subtitleIndex = index),
|
||||
[importMap.isPublic]: (index: number) => (isPublicIndex = index),
|
||||
[importMap.skip]: (index: number) => (skipIndex = index),
|
||||
[importMap.note]: (index: number) => (notesIndex = index),
|
||||
[importMap.colour]: (index: number) => (colourIndex = index),
|
||||
|
||||
[importMap.endAction]: (row: number, col: number) => {
|
||||
endActionIndex = col;
|
||||
rundownMetadata['endAction'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.timerType]: (row: number, col: number) => {
|
||||
timerTypeIndex = col;
|
||||
rundownMetadata['timerType'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.endAction]: (index: number) => (endActionIndex = index),
|
||||
[importMap.timerType]: (index: number) => (timerTypeIndex = index),
|
||||
|
||||
[importMap.user0]: (row: number, col: number) => {
|
||||
user0Index = col;
|
||||
rundownMetadata['user0'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user1]: (row: number, col: number) => {
|
||||
user1Index = col;
|
||||
rundownMetadata['user1'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user2]: (row: number, col: number) => {
|
||||
user2Index = col;
|
||||
rundownMetadata['user2'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user3]: (row: number, col: number) => {
|
||||
user3Index = col;
|
||||
rundownMetadata['user3'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user4]: (row: number, col: number) => {
|
||||
user4Index = col;
|
||||
rundownMetadata['user4'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user5]: (row: number, col: number) => {
|
||||
user5Index = col;
|
||||
rundownMetadata['user5'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user6]: (row: number, col: number) => {
|
||||
user6Index = col;
|
||||
rundownMetadata['user6'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user7]: (row: number, col: number) => {
|
||||
user7Index = col;
|
||||
rundownMetadata['user7'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user8]: (row: number, col: number) => {
|
||||
user8Index = col;
|
||||
rundownMetadata['user8'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user9]: (row: number, col: number) => {
|
||||
user9Index = col;
|
||||
rundownMetadata['user9'] = { row: row, col: col };
|
||||
},
|
||||
} as const;
|
||||
[importMap.user0]: (index: number) => (user0Index = index),
|
||||
[importMap.user1]: (index: number) => (user1Index = index),
|
||||
[importMap.user2]: (index: number) => (user2Index = index),
|
||||
[importMap.user3]: (index: number) => (user3Index = index),
|
||||
[importMap.user4]: (index: number) => (user4Index = index),
|
||||
[importMap.user5]: (index: number) => (user5Index = index),
|
||||
[importMap.user6]: (index: number) => (user6Index = index),
|
||||
[importMap.user7]: (index: number) => (user7Index = index),
|
||||
[importMap.user8]: (index: number) => (user8Index = index),
|
||||
[importMap.user9]: (index: number) => (user9Index = index),
|
||||
} as const;
|
||||
|
||||
row.forEach((column, j) => {
|
||||
// 1. we check if we have set a flag for a known field
|
||||
if (projectTitleNext) {
|
||||
projectData.title = makeString(column, '');
|
||||
projectTitleNext = false;
|
||||
} else if (projectDescriptionNext) {
|
||||
projectData.description = makeString(column, '');
|
||||
projectDescriptionNext = false;
|
||||
} else if (publicUrlNext) {
|
||||
projectData.publicUrl = makeString(column, '');
|
||||
publicUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
projectData.publicInfo = makeString(column, '');
|
||||
publicInfoNext = false;
|
||||
} else if (backstageUrlNext) {
|
||||
projectData.backstageUrl = makeString(column, '');
|
||||
backstageUrlNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
projectData.backstageInfo = makeString(column, '');
|
||||
backstageInfoNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === durationIndex) {
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = Boolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = Boolean(column);
|
||||
} else if (j === notesIndex) {
|
||||
event.note = makeString(column, '');
|
||||
} else if (j === endActionIndex) {
|
||||
event.endAction = validateEndAction(column);
|
||||
} else if (j === timerTypeIndex) {
|
||||
event.timerType = validateTimerType(column);
|
||||
} else if (j === colourIndex) {
|
||||
event.colour = makeString(column, '');
|
||||
} else if (j === user0Index) {
|
||||
event.user0 = makeString(column, '');
|
||||
} else if (j === user1Index) {
|
||||
event.user1 = makeString(column, '');
|
||||
} else if (j === user2Index) {
|
||||
event.user2 = makeString(column, '');
|
||||
} else if (j === user3Index) {
|
||||
event.user3 = makeString(column, '');
|
||||
} else if (j === user4Index) {
|
||||
event.user4 = makeString(column, '');
|
||||
} else if (j === user5Index) {
|
||||
event.user5 = makeString(column, '');
|
||||
} else if (j === user6Index) {
|
||||
event.user6 = makeString(column, '');
|
||||
} else if (j === user7Index) {
|
||||
event.user7 = makeString(column, '');
|
||||
} else if (j === user8Index) {
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
const col = column.toLowerCase();
|
||||
row.forEach((column, j) => {
|
||||
// 1. we check if we have set a flag for a known field
|
||||
if (projectTitleNext) {
|
||||
projectData.title = makeString(column, '');
|
||||
projectTitleNext = false;
|
||||
} else if (projectDescriptionNext) {
|
||||
projectData.description = makeString(column, '');
|
||||
projectDescriptionNext = false;
|
||||
} else if (publicUrlNext) {
|
||||
projectData.publicUrl = makeString(column, '');
|
||||
publicUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
projectData.publicInfo = makeString(column, '');
|
||||
publicInfoNext = false;
|
||||
} else if (backstageUrlNext) {
|
||||
projectData.backstageUrl = makeString(column, '');
|
||||
backstageUrlNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
projectData.backstageInfo = makeString(column, '');
|
||||
backstageInfoNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === durationIndex) {
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = Boolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = Boolean(column);
|
||||
} else if (j === notesIndex) {
|
||||
event.note = makeString(column, '');
|
||||
} else if (j === endActionIndex) {
|
||||
event.endAction = validateEndAction(column);
|
||||
} else if (j === timerTypeIndex) {
|
||||
event.timerType = validateTimerType(column);
|
||||
} else if (j === colourIndex) {
|
||||
event.colour = makeString(column, '');
|
||||
} else if (j === user0Index) {
|
||||
event.user0 = makeString(column, '');
|
||||
} else if (j === user1Index) {
|
||||
event.user1 = makeString(column, '');
|
||||
} else if (j === user2Index) {
|
||||
event.user2 = makeString(column, '');
|
||||
} else if (j === user3Index) {
|
||||
event.user3 = makeString(column, '');
|
||||
} else if (j === user4Index) {
|
||||
event.user4 = makeString(column, '');
|
||||
} else if (j === user5Index) {
|
||||
event.user5 = makeString(column, '');
|
||||
} else if (j === user6Index) {
|
||||
event.user6 = makeString(column, '');
|
||||
} else if (j === user7Index) {
|
||||
event.user7 = makeString(column, '');
|
||||
} else if (j === user8Index) {
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
const col = column.toLowerCase();
|
||||
|
||||
if (handlers[col]) {
|
||||
handlers[col](rowIndex, j);
|
||||
if (handlers[col]) {
|
||||
handlers[col](j);
|
||||
}
|
||||
// else. we don't know how to handle this column
|
||||
// just ignore it
|
||||
}
|
||||
// else. we don't know how to handle this column
|
||||
// just ignore it
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(event).length > 0) {
|
||||
// if any data was found, push to array
|
||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(event).length > 0) {
|
||||
// if any data was found, push to array
|
||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
rundown,
|
||||
project: projectData,
|
||||
@@ -335,8 +245,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
version: '2.0.0',
|
||||
},
|
||||
userFields: customUserFields,
|
||||
projectMetadata: projectMetadata,
|
||||
rundownMetadata: rundownMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -368,9 +276,7 @@ export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
// Import OSC settings if any
|
||||
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
||||
// Import HTTP settings if any
|
||||
// returnData.http = parseHttp(jsonData, enforce);
|
||||
// Import GoogleSheet settings if any
|
||||
returnData.googleSheet = parseGoogleSheet(jsonData, true);
|
||||
returnData.http = parseHttp(jsonData) ?? dbModel.http;
|
||||
|
||||
return returnData as DatabaseModel;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import {
|
||||
Alias,
|
||||
GoogleSheet,
|
||||
OntimeRundown,
|
||||
HttpSettings,
|
||||
OSCSettings,
|
||||
OscSubscription,
|
||||
OscSubscriptionOptions,
|
||||
ProjectData,
|
||||
Settings,
|
||||
TimerLifeCycle,
|
||||
UserFields,
|
||||
ViewSettings,
|
||||
OscSubscription,
|
||||
HttpSubscription,
|
||||
OscSubscriptionOptions,
|
||||
HttpSubscriptionOptions,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||
@@ -160,12 +162,12 @@ export const parseViewSettings = (data): ViewSettings => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and validates subscription entry
|
||||
* Parses and validates OSC subscription cycle options
|
||||
* @param data
|
||||
*/
|
||||
export const validateOscSubscriptionEntry = (data: OscSubscriptionOptions): boolean => {
|
||||
for (const subscription in data) {
|
||||
if (typeof data[subscription].message !== 'string' || typeof data[subscription].enabled !== 'boolean') {
|
||||
export const validateOscSubscriptionCycle = (data: OscSubscriptionOptions[]): boolean => {
|
||||
for (const subscriptionOption of data) {
|
||||
if (typeof subscriptionOption.message !== 'string' || typeof subscriptionOption.enabled !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -173,22 +175,23 @@ export const validateOscSubscriptionEntry = (data: OscSubscriptionOptions): bool
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and validates subscription object
|
||||
* Parses and validates OSC subscription object
|
||||
* @param data
|
||||
*/
|
||||
export const validateOscObject = (data: OscSubscription): boolean => {
|
||||
export const validateOscSubscriptionObject = (data: OscSubscription): boolean => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timerKeys = Object.keys(TimerLifeCycle);
|
||||
for (const key of timerKeys) {
|
||||
// must contains all keys and be an array
|
||||
if (!(key in data) || !Array.isArray(data[key])) {
|
||||
return false;
|
||||
}
|
||||
for (const subscription of data[key]) {
|
||||
if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
const isValid = validateOscSubscriptionCycle(data[key]);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -201,8 +204,9 @@ export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
|
||||
if ('osc' in data) {
|
||||
console.log('Found OSC definition, importing...');
|
||||
|
||||
// TODO: this can be improved by only merging known keys
|
||||
const loadedConfig = data.osc || {};
|
||||
const validatedSubscriptions = validateOscObject(loadedConfig.subscriptions)
|
||||
const validatedSubscriptions = validateOscSubscriptionObject(loadedConfig.subscriptions)
|
||||
? loadedConfig.subscriptions
|
||||
: dbModel.osc.subscriptions;
|
||||
|
||||
@@ -217,20 +221,63 @@ export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and validates HTTP subscription cycle options
|
||||
* @param data
|
||||
*/
|
||||
export const validateHttpSubscriptionCycle = (data: HttpSubscriptionOptions[]): boolean => {
|
||||
for (const subscriptionOption of data) {
|
||||
const isHttp = subscriptionOption.message?.startsWith('http://');
|
||||
if (typeof subscriptionOption.message !== 'string' || !isHttp || typeof subscriptionOption.enabled !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and validates HTTP subscription object
|
||||
* @param data
|
||||
*/
|
||||
export const validateHttpSubscriptionObject = (data: HttpSubscription): boolean => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const timerKeys = Object.keys(TimerLifeCycle);
|
||||
// must contains all keys and be an array
|
||||
for (const key of timerKeys) {
|
||||
if (!(key in data) || !Array.isArray(data[key])) {
|
||||
return false;
|
||||
}
|
||||
const isValid = validateHttpSubscriptionCycle(data[key]);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse Http portion of an entry
|
||||
* @param {object} data - data object
|
||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseHttp = (data, enforce) => {
|
||||
const newHttp = {};
|
||||
export const parseHttp = (data: { http?: Partial<HttpSettings> }): HttpSettings => {
|
||||
if ('http' in data) {
|
||||
console.log('Found HTTP definition, importing...');
|
||||
} else if (enforce) {
|
||||
/* Not yet */
|
||||
|
||||
// TODO: this can be improved by only merging known keys
|
||||
const loadedConfig = data?.http || {};
|
||||
const validatedSubscriptions = validateHttpSubscriptionObject(loadedConfig.subscriptions)
|
||||
? loadedConfig.subscriptions
|
||||
: dbModel.http.subscriptions;
|
||||
|
||||
return {
|
||||
enabledOut: loadedConfig.enabledOut ?? dbModel.http.enabledOut,
|
||||
subscriptions: validatedSubscriptions,
|
||||
};
|
||||
}
|
||||
return newHttp;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -285,24 +332,3 @@ export const parseUserFields = (data): UserFields => {
|
||||
}
|
||||
return { ...newUserFields };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse Google Sheet portion of an entry
|
||||
* @param {object} data - data object
|
||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseGoogleSheet = (data, enforce) => {
|
||||
const newSheet: GoogleSheet = {
|
||||
id: '',
|
||||
worksheet: '',
|
||||
};
|
||||
if ('googleSheet' in data) {
|
||||
console.log('Found Google Sheet definition, importing...');
|
||||
newSheet.id ??= data.googleSheet?.id;
|
||||
newSheet.worksheet ??= data.googleSheet?.worksheet;
|
||||
return newSheet;
|
||||
} else if (enforce) {
|
||||
return newSheet;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { getAppDataPath } from '../setup.js';
|
||||
import { DatabaseModel, LogOrigin } from 'ontime-types';
|
||||
import { parseExcel } from './parser.js';
|
||||
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
|
||||
import { ensureDirectory } from './fileManagement.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { GoogleSheetState } from 'ontime-types';
|
||||
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './googleSheetUtils.js';
|
||||
|
||||
type ResponseOK = {
|
||||
data: Partial<DatabaseModel>;
|
||||
};
|
||||
|
||||
class sheet {
|
||||
private static client: null | OAuth2Client = null;
|
||||
private readonly scope = 'https://www.googleapis.com/auth/spreadsheets';
|
||||
private readonly sheetsFolder = `${getAppDataPath()}/sheets`;
|
||||
private readonly client_secret = `${this.sheetsFolder}/client_secret.json`;
|
||||
private static authUrl: null | string = null;
|
||||
private worksheetId: number = 0;
|
||||
private sheetId: string = '';
|
||||
private range: string = '';
|
||||
|
||||
public async getSheetState(): Promise<GoogleSheetState> {
|
||||
const ret: GoogleSheetState = {
|
||||
auth: false,
|
||||
id: false,
|
||||
worksheet: false,
|
||||
};
|
||||
this.sheetId = '';
|
||||
this.worksheetId = 0;
|
||||
if (!sheet.client) {
|
||||
return ret;
|
||||
}
|
||||
try {
|
||||
ret.auth = await this.refreshToken();
|
||||
if (ret.auth) {
|
||||
const settings = DataProvider.getGoogleSheet();
|
||||
const x = await this.exist(settings.id, settings.worksheet);
|
||||
if (x === true) {
|
||||
ret.id = true;
|
||||
this.sheetId = settings.id;
|
||||
} else if (x !== false) {
|
||||
ret.id = true;
|
||||
ret.worksheet = true;
|
||||
this.sheetId = settings.id;
|
||||
this.worksheetId = x.worksheetId;
|
||||
this.range = x.range;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(LogOrigin.Server, `Google Sheet: Faild to refresh token ${err}`);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* test existence of sheet and worksheet
|
||||
* @param {string} sheetId - https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
||||
* @param {string} worksheet - the name of the worksheet containing ontime data
|
||||
* @returns {Promise<false | {worksheetId: number, range: string}>} - false if not found | true if sheetId existes | id of worksheet and rage of worksheet
|
||||
* @throws
|
||||
*/
|
||||
private async exist(
|
||||
sheetId: string,
|
||||
worksheet: string,
|
||||
): Promise<false | true | { worksheetId: number; range: string }> {
|
||||
const spreadsheets = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.get({
|
||||
spreadsheetId: sheetId,
|
||||
});
|
||||
|
||||
if (spreadsheets.status === 200) {
|
||||
const w = spreadsheets.data.sheets.find((p) => p.properties.title == worksheet);
|
||||
if (w !== undefined) {
|
||||
const endCell = getA1Notation(w.properties.gridProperties.rowCount, w.properties.gridProperties.columnCount);
|
||||
return { worksheetId: w.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* push sheet
|
||||
* @throws
|
||||
*/
|
||||
public async push() {
|
||||
const { auth, id, worksheet } = await this.getSheetState();
|
||||
if (!auth && !id && !worksheet) {
|
||||
throw new Error(`Sheet not authorized or incorrect ID or worksheet`);
|
||||
}
|
||||
|
||||
const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({
|
||||
spreadsheetId: this.sheetId,
|
||||
valueRenderOption: 'FORMATTED_VALUE',
|
||||
majorDimension: 'ROWS',
|
||||
range: this.range,
|
||||
});
|
||||
if (rq.status === 200) {
|
||||
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values);
|
||||
const rundown = DataProvider.getRundown();
|
||||
const projectData = DataProvider.getProjectData();
|
||||
const titleRow = Object.values(rundownMetadata)[0]['row'];
|
||||
|
||||
const updateRundown = Array<sheets_v4.Schema$Request>();
|
||||
|
||||
// we can't delete the last unflozzen row so we create an empty one
|
||||
updateRundown.push({
|
||||
insertDimension: {
|
||||
inheritFromBefore: false,
|
||||
range: {
|
||||
dimension: 'ROWS',
|
||||
startIndex: titleRow + 1,
|
||||
endIndex: titleRow + 2,
|
||||
sheetId: this.worksheetId,
|
||||
},
|
||||
},
|
||||
});
|
||||
//and delete the rest
|
||||
updateRundown.push({
|
||||
deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: this.worksheetId } },
|
||||
});
|
||||
// insert the lenght of the rundown
|
||||
updateRundown.push({
|
||||
insertDimension: {
|
||||
inheritFromBefore: false,
|
||||
range: {
|
||||
dimension: 'ROWS',
|
||||
startIndex: titleRow + 1,
|
||||
endIndex: titleRow + rundown.length,
|
||||
sheetId: this.worksheetId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
//update the corresponding row with event data
|
||||
rundown.forEach((entry, index) =>
|
||||
updateRundown.push(cellRequenstFromEvent(entry, index, this.worksheetId, rundownMetadata)),
|
||||
);
|
||||
|
||||
//update project data
|
||||
updateRundown.push(cellRequenstFromProjectData(projectData, this.worksheetId, projectMetadata));
|
||||
|
||||
const writeResponds = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.batchUpdate({
|
||||
spreadsheetId: this.sheetId,
|
||||
requestBody: {
|
||||
includeSpreadsheetInResponse: false,
|
||||
responseRanges: [this.range],
|
||||
requests: updateRundown,
|
||||
},
|
||||
});
|
||||
|
||||
if (writeResponds.status == 200) {
|
||||
logger.info(LogOrigin.Server, `Sheet write: ${writeResponds.statusText}`);
|
||||
} else {
|
||||
throw new Error(`Sheet write faild: ${writeResponds.statusText}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Sheet read faild: ${rq.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pull sheet
|
||||
* @returns {Promise<Partial<ResponseOK>>}
|
||||
* @throws
|
||||
*/
|
||||
public async pull(): Promise<Partial<ResponseOK>> {
|
||||
const { auth, id, worksheet } = await this.getSheetState();
|
||||
if (!auth && !id && !worksheet) {
|
||||
throw new Error(`Sheet not authorized or incorrect ID or worksheet`);
|
||||
}
|
||||
|
||||
const res: Partial<ResponseOK> = {};
|
||||
|
||||
const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({
|
||||
spreadsheetId: this.sheetId,
|
||||
valueRenderOption: 'FORMATTED_VALUE',
|
||||
majorDimension: 'ROWS',
|
||||
range: this.range,
|
||||
});
|
||||
if (rq.status === 200) {
|
||||
res.data = {};
|
||||
const dataFromSheet = parseExcel(rq.data.values);
|
||||
res.data.rundown = parseRundown(dataFromSheet);
|
||||
if (res.data.rundown.length < 1) {
|
||||
throw new Error(`Could not find data to import in the worksheet`);
|
||||
}
|
||||
res.data.project = parseProject(dataFromSheet);
|
||||
res.data.userFields = parseUserFields(dataFromSheet);
|
||||
return res;
|
||||
} else {
|
||||
throw new Error(`Sheet read faild: ${rq.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* saves secrets object to appdata path as client_secret.json
|
||||
* @param {object} secrets
|
||||
* @throws
|
||||
*/
|
||||
public async saveClientSecrets(secrets: object) {
|
||||
sheet.client = null;
|
||||
sheet.authUrl = null;
|
||||
ensureDirectory(this.sheetsFolder);
|
||||
if (
|
||||
'client_id' in secrets ||
|
||||
!('project_id' in secrets) ||
|
||||
!('auth_uri' in secrets) ||
|
||||
!('token_uri' in secrets) ||
|
||||
!('auth_provider_x509_cert_url' in secrets) ||
|
||||
!('client_secret' in secrets) ||
|
||||
!('redirect_uris' in secrets)
|
||||
) {
|
||||
throw new Error('Sheet slient secret is missing some keys');
|
||||
}
|
||||
await writeFile(this.client_secret, JSON.stringify(secrets), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* refresh the client token
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async refreshToken(): Promise<boolean> {
|
||||
if (!sheet.client?.credentials?.refresh_token) return false;
|
||||
try {
|
||||
const response = await sheet.client.refreshAccessToken();
|
||||
if (response?.credentials) {
|
||||
return true;
|
||||
}
|
||||
} catch (_) {
|
||||
logger.info(LogOrigin.Server, 'Sheets token expired');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private authServerTimeout;
|
||||
/**
|
||||
* create local Auth Server
|
||||
* @returns {Promise<string | false>} - returns url to serve on success
|
||||
* @throws
|
||||
*/
|
||||
public async openAuthServer(): Promise<string | false> {
|
||||
//TODO: this only works on local networks
|
||||
if (sheet.authUrl) {
|
||||
clearTimeout(this.authServerTimeout);
|
||||
this.authServerTimeout = setTimeout(
|
||||
() => {
|
||||
sheet.authUrl = null;
|
||||
server.unref;
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
return sheet.authUrl;
|
||||
}
|
||||
const creadFile = await readFile(this.client_secret, 'utf-8').catch((err) =>
|
||||
logger.error(LogOrigin.Server, `${err}`),
|
||||
);
|
||||
if (!creadFile) {
|
||||
return false;
|
||||
}
|
||||
const keyFile = JSON.parse(creadFile);
|
||||
const keys = keyFile.installed || keyFile.web;
|
||||
if (!keys.redirect_uris || keys.redirect_uris.length === 0) {
|
||||
logger.error(LogOrigin.Server, `${invalidRedirectUri}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create an oAuth client to authorize the API call
|
||||
const redirectUri = new URL(keys.redirect_uris[0]);
|
||||
if (redirectUri.hostname !== 'localhost') {
|
||||
throw new Error(invalidRedirectUri);
|
||||
}
|
||||
|
||||
// create an oAuth client to authorize the API call
|
||||
const client = new OAuth2Client({
|
||||
clientId: keys.client_id,
|
||||
clientSecret: keys.client_secret,
|
||||
});
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const serverUrl = new URL(req.url, 'http://localhost:3000');
|
||||
if (serverUrl.pathname !== redirectUri.pathname) {
|
||||
res.end('Invalid callback URL');
|
||||
return;
|
||||
}
|
||||
const searchParams = serverUrl.searchParams;
|
||||
if (searchParams.has('error')) {
|
||||
res.end('Authorization rejected.');
|
||||
logger.info(LogOrigin.Server, `Sheet: ${searchParams.get('error')}`);
|
||||
return;
|
||||
}
|
||||
if (!searchParams.has('code')) {
|
||||
res.end('No authentication code provided.');
|
||||
logger.info(LogOrigin.Server, `Sheet: Cannot read authentication code`);
|
||||
return;
|
||||
}
|
||||
const code = searchParams.get('code');
|
||||
const { tokens } = await client.getToken({
|
||||
code: code,
|
||||
redirect_uri: redirectUri.toString(),
|
||||
});
|
||||
client.credentials = tokens;
|
||||
sheet.client = client;
|
||||
res.end('Authentication successful! Please close this tab and return to OnTime.');
|
||||
logger.info(LogOrigin.Server, `Sheet: Authentication successful`);
|
||||
} catch (e) {
|
||||
logger.error(LogOrigin.Server, `Sheet: ${e}`);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
let listenPort = 3000;
|
||||
if (keyFile.installed) {
|
||||
// Use emphemeral port if not a web client
|
||||
listenPort = 0;
|
||||
} else if (redirectUri.port !== '') {
|
||||
listenPort = Number(redirectUri.port);
|
||||
}
|
||||
//TODO: the server might not start correctly
|
||||
server.listen(listenPort, () => {});
|
||||
const address = server.address();
|
||||
if (typeof address !== 'string') {
|
||||
redirectUri.port = String(address.port);
|
||||
}
|
||||
// open the browser to the authorize url to start the workflow
|
||||
const authorizeUrl = client.generateAuthUrl({
|
||||
redirect_uri: redirectUri.toString(),
|
||||
access_type: 'offline',
|
||||
scope: this.scope,
|
||||
});
|
||||
sheet.authUrl = authorizeUrl;
|
||||
this.authServerTimeout = setTimeout(
|
||||
() => {
|
||||
sheet.authUrl = null;
|
||||
server.unref;
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
return authorizeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright 2020 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//TODO: add modification notifications as requrirde by the license
|
||||
|
||||
const invalidRedirectUri = `The provided keyfile does not define a valid
|
||||
redirect URI. There must be at least one redirect URI defined, and this sample
|
||||
assumes it redirects to 'http://localhost:3000/oauth2callback'. Please edit
|
||||
your keyfile, and add a 'redirect_uris' section. For example:
|
||||
|
||||
"redirect_uris": [
|
||||
"http://localhost:3000/oauth2callback"
|
||||
]
|
||||
`;
|
||||
|
||||
export const Sheet = new sheet();
|
||||
@@ -259,6 +259,17 @@
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabled": true
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": false,
|
||||
"subscriptions": {
|
||||
"onLoad": [],
|
||||
"onStart": [],
|
||||
"onPause": [],
|
||||
"onStop": [],
|
||||
"onUpdate": [],
|
||||
"onFinish": []
|
||||
}
|
||||
},
|
||||
"aliases": [
|
||||
{
|
||||
"enabled": true,
|
||||
|
||||
+18
-7
@@ -29,8 +29,8 @@
|
||||
"user9": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "aa42f",
|
||||
"cue": "1"
|
||||
"cue": "1",
|
||||
"id": "aa42f"
|
||||
},
|
||||
{
|
||||
"title": "title 2",
|
||||
@@ -57,8 +57,8 @@
|
||||
"user9": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "d71bc",
|
||||
"cue": "2"
|
||||
"cue": "2",
|
||||
"id": "d71bc"
|
||||
},
|
||||
{
|
||||
"title": "title 3",
|
||||
@@ -85,8 +85,8 @@
|
||||
"user9": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "da5b4",
|
||||
"cue": "3"
|
||||
"cue": "3",
|
||||
"id": "da5b4"
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"app": "ontime",
|
||||
"version": "2.0.0",
|
||||
"version": "2.24.8",
|
||||
"serverPort": 4001,
|
||||
"editorKey": null,
|
||||
"operatorKey": null,
|
||||
@@ -148,5 +148,16 @@
|
||||
"onUpdate": [],
|
||||
"onFinish": []
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": {
|
||||
"onLoad": [],
|
||||
"onStart": [],
|
||||
"onPause": [],
|
||||
"onStop": [],
|
||||
"onUpdate": [],
|
||||
"onFinish": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,5 +468,16 @@
|
||||
],
|
||||
"onFinish": []
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": {
|
||||
"onLoad": [],
|
||||
"onStart": [],
|
||||
"onPause": [],
|
||||
"onStop": [],
|
||||
"onUpdate": [],
|
||||
"onFinish": []
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "2.16.2",
|
||||
"version": "2.24.8",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
"lightdev",
|
||||
"ontime",
|
||||
"timer",
|
||||
"rundown"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js';
|
||||
import { Settings } from './core/Settings.type.js';
|
||||
import { UserFields } from './core/UserFields.type.js';
|
||||
import { ViewSettings } from './core/Views.type.js';
|
||||
import { GoogleSheet } from '../index.js';
|
||||
import { HttpSettings } from '../index.js';
|
||||
|
||||
export type DatabaseModel = {
|
||||
rundown: OntimeRundown;
|
||||
@@ -14,6 +14,6 @@ export type DatabaseModel = {
|
||||
viewSettings: ViewSettings;
|
||||
aliases: Alias[];
|
||||
userFields: UserFields;
|
||||
googleSheet: GoogleSheet;
|
||||
osc: OSCSettings;
|
||||
http: HttpSettings;
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export type GoogleSheet = {
|
||||
worksheet: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GoogleSheetState = {
|
||||
auth: boolean;
|
||||
id: boolean;
|
||||
worksheet: boolean;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Subscription } from './Subscription.type.js';
|
||||
|
||||
export type HttpSubscriptionOptions = { message: string; enabled: boolean };
|
||||
export type HttpSubscription = Subscription<HttpSubscriptionOptions>;
|
||||
|
||||
export interface HttpSettings {
|
||||
enabledOut: boolean;
|
||||
subscriptions: HttpSubscription;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
||||
import { Subscription } from './Subscription.type.js';
|
||||
|
||||
export type OscSubscriptionOptions = { message: string; enabled: boolean };
|
||||
export type OscSubscription = { [key in TimerLifeCycleKey]: OscSubscriptionOptions[] };
|
||||
export type OscSubscription = Subscription<OscSubscriptionOptions>;
|
||||
|
||||
export interface OSCSettings {
|
||||
portIn: number;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TimerLifeCycleKey } from './TimerLifecycle.type.js';
|
||||
|
||||
export type Subscription<T> = { [key in TimerLifeCycleKey]: T[] };
|
||||
@@ -28,13 +28,14 @@ export type { Alias } from './definitions/core/Alias.type.js';
|
||||
// ---> User Fields
|
||||
export type { UserFields } from './definitions/core/UserFields.type.js';
|
||||
|
||||
// ---> Integration, Subscription
|
||||
export type { Subscription } from './definitions/core/Subscription.type.js';
|
||||
|
||||
// ---> OSC
|
||||
export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './definitions/core/OscSettings.type.js';
|
||||
|
||||
// ---> HTTP
|
||||
|
||||
// ---> Google Sheet
|
||||
export type { GoogleSheet, GoogleSheetState } from './definitions/core/GoogleSheet.type.js';
|
||||
export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js';
|
||||
|
||||
// SERVER RESPONSES
|
||||
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';
|
||||
|
||||
@@ -15,6 +15,7 @@ export { formatDisplay } from './src/date-utils/formatDisplay.js';
|
||||
export { formatFromMillis } from './src/date-utils/formatFromMillis.js';
|
||||
export { isTimeString } from './src/date-utils/isTimeString.js';
|
||||
export { millisToString } from './src/date-utils/millisToString.js';
|
||||
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
||||
|
||||
// time utils
|
||||
export { dayInMs, mts } from './src/timeConstants.js';
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.30.1"
|
||||
}
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { isColourHex } from './isColourHex';
|
||||
|
||||
describe('test isColourHex() function', () => {
|
||||
it('it validates colour hex strings', () => {
|
||||
const ts = ['#FFF', '#FFFF', '#FFFFFF', '#FFFFFFFF'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it validates colour hex strings', () => {
|
||||
const ts = ['#F90', '#1234', '#56789A', '#BCDEF012'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it validates colour hex strings', () => {
|
||||
const ts = ['#f90', '#1234', '#56789a', '#bcdef012'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it fails digits bigger than F', () => {
|
||||
const ts = ['#FFG'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('it fails incorect amout of digits', () => {
|
||||
const ts = ['#F', '#FF', '#FFFFF', '#FFFFFFF', '#FFFFFFFFF'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('it fails missing #', () => {
|
||||
const ts = ['FFF', 'FFFF', 'FFFFFF', 'FFFFFFFF'];
|
||||
for (const s of ts) {
|
||||
expect(isColourHex(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @description Validates a colour hex string
|
||||
* @param {string} text - colour hex string "#FFF" | "#FFFF" | "#FFFFFF" | "#FFFFFFFF"
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
export const isColourHex = (text: string): boolean => {
|
||||
const regexS = /^#((?:[a-f\d]{1}){3,4})$/i;
|
||||
const regexD = /^#((?:[a-f\d]{2}){3,4})$/i;
|
||||
return regexS.test(text) || regexD.test(text);
|
||||
};
|
||||
Generated
+108
-181
@@ -249,9 +249,6 @@ importers:
|
||||
|
||||
apps/server:
|
||||
dependencies:
|
||||
'@googleapis/sheets':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
body-parser:
|
||||
specifier: ^1.20.0
|
||||
version: 1.20.1
|
||||
@@ -273,9 +270,9 @@ importers:
|
||||
express-validator:
|
||||
specifier: ^6.14.2
|
||||
version: 6.14.2
|
||||
google-auth-library:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
got:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
lowdb:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
@@ -2231,16 +2228,6 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@googleapis/sheets@5.0.5:
|
||||
resolution: {integrity: sha512-XMoONmgAJm2jYeTYHX4054VcEkElxlgqmnHvt0wAurzEHoGJLdUHhTAJXGPLgSs4WVMPtgU8HLrmk7/U+Qlw7A==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
googleapis-common: 7.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.13:
|
||||
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -2588,6 +2575,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/@sindresorhus/is@6.1.0:
|
||||
resolution: {integrity: sha512-BuvU07zq3tQ/2SIgBsEuxKYDyDjC0n7Zir52bpHy2xnBbW81+po43aLFPLbeV3HRAheFbGud1qgcqSYfhtHMAg==}
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.20.12):
|
||||
resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2720,6 +2712,13 @@ packages:
|
||||
defer-to-connect: 2.0.1
|
||||
dev: true
|
||||
|
||||
/@szmarczak/http-timer@5.0.1:
|
||||
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
defer-to-connect: 2.0.1
|
||||
dev: false
|
||||
|
||||
/@tanstack/eslint-plugin-query@5.8.4(eslint@8.53.0)(typescript@5.2.2):
|
||||
resolution: {integrity: sha512-KVgcMc+Bn1qbwkxYVWQoiVSNEIN4IAiLj3cUH/SAHT8m8E59Y97o8ON1syp0Rcw094ItG8pEVZFyQuOaH6PDgQ==}
|
||||
peerDependencies:
|
||||
@@ -2976,6 +2975,10 @@ packages:
|
||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||
dev: true
|
||||
|
||||
/@types/http-cache-semantics@4.0.4:
|
||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||
dev: false
|
||||
|
||||
/@types/istanbul-lib-coverage@2.0.4:
|
||||
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
|
||||
dev: true
|
||||
@@ -3051,10 +3054,6 @@ packages:
|
||||
resolution: {integrity: sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==}
|
||||
dev: true
|
||||
|
||||
/@types/node@18.15.11:
|
||||
resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==}
|
||||
dev: true
|
||||
|
||||
/@types/parse-json@4.0.0:
|
||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||
|
||||
@@ -3536,15 +3535,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/agent-base@7.1.0:
|
||||
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/ajv-keywords@3.5.2(ajv@6.12.6):
|
||||
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
|
||||
peerDependencies:
|
||||
@@ -3797,16 +3787,13 @@ packages:
|
||||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/big-integer@1.6.51:
|
||||
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: true
|
||||
|
||||
/bignumber.js@9.1.2:
|
||||
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
||||
dev: false
|
||||
|
||||
/binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3909,10 +3896,6 @@ packages:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: true
|
||||
|
||||
/buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
dev: false
|
||||
|
||||
/buffer-equal@1.0.0:
|
||||
resolution: {integrity: sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -3997,6 +3980,24 @@ packages:
|
||||
engines: {node: '>=10.6.0'}
|
||||
dev: true
|
||||
|
||||
/cacheable-lookup@7.0.0:
|
||||
resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: false
|
||||
|
||||
/cacheable-request@10.2.14:
|
||||
resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
'@types/http-cache-semantics': 4.0.4
|
||||
get-stream: 6.0.1
|
||||
http-cache-semantics: 4.1.1
|
||||
keyv: 4.5.4
|
||||
mimic-response: 4.0.0
|
||||
normalize-url: 8.0.0
|
||||
responselike: 3.0.0
|
||||
dev: false
|
||||
|
||||
/cacheable-request@7.0.2:
|
||||
resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4424,6 +4425,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
@@ -4434,7 +4436,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
dev: true
|
||||
|
||||
/deep-eql@4.1.3:
|
||||
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
|
||||
@@ -4500,7 +4501,6 @@ packages:
|
||||
/defer-to-connect@2.0.1:
|
||||
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/define-lazy-prop@3.0.0:
|
||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||
@@ -4654,12 +4654,6 @@ packages:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
dev: true
|
||||
|
||||
/ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
dev: false
|
||||
@@ -5306,10 +5300,6 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
dev: false
|
||||
|
||||
/extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
@@ -5453,6 +5443,11 @@ packages:
|
||||
is-callable: 1.2.7
|
||||
dev: true
|
||||
|
||||
/form-data-encoder@4.0.2:
|
||||
resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
|
||||
engines: {node: '>= 18'}
|
||||
dev: false
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -5567,30 +5562,6 @@ packages:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
dev: true
|
||||
|
||||
/gaxios@6.1.1:
|
||||
resolution: {integrity: sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.2
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gcp-metadata@6.1.0:
|
||||
resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
gaxios: 6.1.1
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -5635,12 +5606,10 @@ packages:
|
||||
/get-stream@6.0.1:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/get-stream@8.0.1:
|
||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||
engines: {node: '>=16'}
|
||||
dev: true
|
||||
|
||||
/get-symbol-description@1.0.0:
|
||||
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
|
||||
@@ -5724,36 +5693,6 @@ packages:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
dev: true
|
||||
|
||||
/google-auth-library@9.2.0:
|
||||
resolution: {integrity: sha512-1oV3p0JhNEhVbj26eF3FAJcv9MXXQt4S0wcvKZaDbl4oHq5V3UJoSbsGZGQNcjoCdhW4kDSwOs11wLlHog3fgQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 6.1.1
|
||||
gcp-metadata: 6.1.0
|
||||
gtoken: 7.0.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/googleapis-common@7.0.1:
|
||||
resolution: {integrity: sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 6.1.1
|
||||
google-auth-library: 9.2.0
|
||||
qs: 6.11.0
|
||||
url-template: 2.0.8
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
dependencies:
|
||||
@@ -5777,6 +5716,23 @@ packages:
|
||||
responselike: 2.0.1
|
||||
dev: true
|
||||
|
||||
/got@14.0.0:
|
||||
resolution: {integrity: sha512-X01vTgaX9SwaMq5DfImvS+3GMQFFs5HtrrlS9CuzUSzkxAf/tWGEyynuI+Qy7BjciMczZGjyVSmawYbP4eYhYA==}
|
||||
engines: {node: '>=20'}
|
||||
dependencies:
|
||||
'@sindresorhus/is': 6.1.0
|
||||
'@szmarczak/http-timer': 5.0.1
|
||||
cacheable-lookup: 7.0.0
|
||||
cacheable-request: 10.2.14
|
||||
decompress-response: 6.0.0
|
||||
form-data-encoder: 4.0.2
|
||||
get-stream: 8.0.1
|
||||
http2-wrapper: 2.2.1
|
||||
lowercase-keys: 3.0.0
|
||||
p-cancelable: 4.0.1
|
||||
responselike: 3.0.0
|
||||
dev: false
|
||||
|
||||
/graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
dev: true
|
||||
@@ -5789,17 +5745,6 @@ packages:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
dev: true
|
||||
|
||||
/gtoken@7.0.1:
|
||||
resolution: {integrity: sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dependencies:
|
||||
gaxios: 6.1.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/has-bigints@1.0.2:
|
||||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||
dev: true
|
||||
@@ -5863,7 +5808,6 @@ packages:
|
||||
|
||||
/http-cache-semantics@4.1.1:
|
||||
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
||||
dev: true
|
||||
|
||||
/http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
@@ -5895,6 +5839,14 @@ packages:
|
||||
resolve-alpn: 1.2.1
|
||||
dev: true
|
||||
|
||||
/http2-wrapper@2.2.1:
|
||||
resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==}
|
||||
engines: {node: '>=10.19.0'}
|
||||
dependencies:
|
||||
quick-lru: 5.1.1
|
||||
resolve-alpn: 1.2.1
|
||||
dev: false
|
||||
|
||||
/https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -5905,16 +5857,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/https-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
debug: 4.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -6193,6 +6135,7 @@ packages:
|
||||
/is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
@@ -6397,15 +6340,8 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
dependencies:
|
||||
bignumber.js: 9.1.2
|
||||
dev: false
|
||||
|
||||
/json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
dev: true
|
||||
|
||||
/json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
@@ -6456,21 +6392,6 @@ packages:
|
||||
object.assign: 4.1.4
|
||||
dev: true
|
||||
|
||||
/jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/keyv@4.5.2:
|
||||
resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
|
||||
dependencies:
|
||||
@@ -6481,7 +6402,6 @@ packages:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
dev: true
|
||||
|
||||
/lazy-val@1.0.5:
|
||||
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
||||
@@ -6600,6 +6520,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/lowercase-keys@3.0.0:
|
||||
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
@@ -6737,7 +6662,11 @@ packages:
|
||||
/mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/mimic-response@4.0.0:
|
||||
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/min-indent@1.0.1:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
@@ -6815,6 +6744,7 @@ packages:
|
||||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
@@ -6871,18 +6801,6 @@ packages:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: false
|
||||
|
||||
/node-osc@9.0.2:
|
||||
resolution: {integrity: sha512-q+VQL7DMWRL5+yvzRlWVig8BD9raotLs6onHU4e8MaFgxmYuIwcXhsvQeyUZFiKP6y/qGUXU6K0T99gVmISwmA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
@@ -6936,6 +6854,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/normalize-url@8.0.0:
|
||||
resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: false
|
||||
|
||||
/npm-run-path@4.0.1:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7096,6 +7019,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/p-cancelable@4.0.1:
|
||||
resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: false
|
||||
|
||||
/p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7367,7 +7295,6 @@ packages:
|
||||
/quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/random-bytes@1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
@@ -7636,7 +7563,6 @@ packages:
|
||||
|
||||
/resolve-alpn@1.2.1:
|
||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||
dev: true
|
||||
|
||||
/resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
@@ -7665,6 +7591,13 @@ packages:
|
||||
lowercase-keys: 2.0.0
|
||||
dev: true
|
||||
|
||||
/responselike@3.0.0:
|
||||
resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
lowercase-keys: 3.0.0
|
||||
dev: false
|
||||
|
||||
/restore-cursor@4.0.0:
|
||||
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
@@ -8270,6 +8203,7 @@ packages:
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/tr46@3.0.0:
|
||||
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
|
||||
@@ -8573,10 +8507,6 @@ packages:
|
||||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
dev: false
|
||||
|
||||
/use-callback-ref@1.3.0(@types/react@18.0.26)(react@18.2.0):
|
||||
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8629,11 +8559,6 @@ packages:
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dev: false
|
||||
|
||||
/uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
dev: true
|
||||
@@ -8680,7 +8605,7 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-node@0.30.1(@types/node@18.15.11)(sass@1.57.1):
|
||||
/vite-node@0.30.1(@types/node@16.18.23)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==}
|
||||
engines: {node: '>=v14.18.0'}
|
||||
hasBin: true
|
||||
@@ -8690,7 +8615,7 @@ packages:
|
||||
mlly: 1.2.0
|
||||
pathe: 1.1.0
|
||||
picocolors: 1.0.0
|
||||
vite: 4.3.1(@types/node@18.15.11)(sass@1.57.1)
|
||||
vite: 4.3.1(@types/node@16.18.23)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -8806,7 +8731,7 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vite@4.3.1(@types/node@18.15.11)(sass@1.57.1):
|
||||
/vite@4.3.1(@types/node@16.18.23)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
@@ -8831,7 +8756,7 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.15.11
|
||||
'@types/node': 16.18.23
|
||||
esbuild: 0.17.5
|
||||
postcss: 8.4.21
|
||||
rollup: 3.20.7
|
||||
@@ -8939,7 +8864,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/chai': 4.3.4
|
||||
'@types/chai-subset': 1.3.3
|
||||
'@types/node': 18.15.11
|
||||
'@types/node': 16.18.23
|
||||
'@vitest/expect': 0.30.1
|
||||
'@vitest/runner': 0.30.1
|
||||
'@vitest/snapshot': 0.30.1
|
||||
@@ -8961,8 +8886,8 @@ packages:
|
||||
strip-literal: 1.0.1
|
||||
tinybench: 2.4.0
|
||||
tinypool: 0.4.0
|
||||
vite: 4.3.1(@types/node@18.15.11)(sass@1.57.1)
|
||||
vite-node: 0.30.1(@types/node@18.15.11)(sass@1.57.1)
|
||||
vite: 4.3.1(@types/node@16.18.23)(sass@1.57.1)
|
||||
vite-node: 0.30.1(@types/node@16.18.23)(sass@1.57.1)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
@@ -8986,6 +8911,7 @@ packages:
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
@@ -9031,6 +8957,7 @@ packages:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
|
||||
/which-boxed-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
|
||||
Reference in New Issue
Block a user