mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 22:47:59 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac5c0a9a2 | |||
| 649f0af2cf | |||
| fd9b944ecf | |||
| 0b36608134 | |||
| bac11adeb1 | |||
| 31799f8fab | |||
| af177060ff | |||
| d26a3cd393 | |||
| 38a3165a79 | |||
| 1715c19747 | |||
| 6be6e5fd8d | |||
| 44b0cfd928 | |||
| 3d50b8ce49 | |||
| c3d66f5bd1 | |||
| d08787a6ba | |||
| f0e6231338 | |||
| 430bfb38f2 | |||
| 60354f8ea3 | |||
| bc12531729 | |||
| 0b8a694e4f | |||
| 73ada99596 | |||
| f509655d31 | |||
| fb1bbd6d5c | |||
| 221ff7b3af |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "3.11.0-beta.1",
|
||||
"version": "3.11.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "3.11.0-beta.1",
|
||||
"version": "3.11.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -44,6 +44,8 @@ const SPublic = withPreset(withData(Public));
|
||||
const SLowerThird = withPreset(withData(Lower));
|
||||
const SStudio = withPreset(withData(StudioClock));
|
||||
const STimeline = withPreset(withData(Timeline));
|
||||
const PCuesheet = withPreset(Cuesheet);
|
||||
const POperator = withPreset(Operator);
|
||||
|
||||
const EditorFeatureWrapper = React.lazy(() => import('./features/EditorFeatureWrapper'));
|
||||
const RundownPanel = React.lazy(() => import('./features/rundown/RundownExport'));
|
||||
@@ -155,8 +157,8 @@ export default function AppRouter() {
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<Editor />} />
|
||||
<Route path='/cuesheet' element={<Cuesheet />} />
|
||||
<Route path='/op' element={<Operator />} />
|
||||
<Route path='/cuesheet' element={<PCuesheet />} />
|
||||
<Route path='/op' element={<POperator />} />
|
||||
|
||||
{/*/!* Protected Routes - Elements *!/*/}
|
||||
<Route
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -15,7 +16,7 @@ export default function FloatingNavigation(props: FloatingNavigationProps) {
|
||||
const isButtonShown = useFadeOutOnInactivity();
|
||||
|
||||
return (
|
||||
<div className={`${style.buttonContainer} ${!isButtonShown ? style.hidden : ''}`}>
|
||||
<div className={cx([style.fadeable, style.buttonContainer, !isButtonShown && style.hidden])}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
|
||||
@@ -7,24 +7,38 @@ $icon-color: $ui-white;
|
||||
$button-bg: $gray-1050;
|
||||
$button-size: 3rem;
|
||||
|
||||
.fadeable {
|
||||
opacity: 1;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lockIcon {
|
||||
font-size: 1.5rem;
|
||||
color: $active-indicator;
|
||||
padding: 0.5em;
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
padding: 0.5em;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.3s;
|
||||
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 12;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -38,16 +52,6 @@ $button-size: 3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.lockIcon {
|
||||
font-size: 1.5rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
color: $active-indicator;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
@extend .button;
|
||||
z-index: 3;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
|
||||
|
||||
import { useFadeOutOnInactivity } from '../../../common/hooks/useFadeOutOnInactivity';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -8,8 +9,8 @@ export default function ViewLockedIcon() {
|
||||
const isLockIconShown = useFadeOutOnInactivity();
|
||||
|
||||
return (
|
||||
<div className={`${style.buttonContainer} ${!isLockIconShown ? style.hidden : ''}`}>
|
||||
<IoLockClosedOutline className={style.lockIcon} />
|
||||
<div className={cx([style.fadeable, style.lockIcon, !isLockIconShown && style.hidden])}>
|
||||
<IoLockClosedOutline />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { memo } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
|
||||
import FloatingNavigation from './FloatingNavigation';
|
||||
import NavigationMenu from './NavigationMenu';
|
||||
import useViewEditor from './useViewEditor';
|
||||
import ViewLockedIcon from './ViewLockedIcon';
|
||||
|
||||
function ViewNavigationMenu() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
interface ViewNavigationMenuProps {
|
||||
isLockable?: boolean;
|
||||
}
|
||||
|
||||
function ViewNavigationMenu(props: ViewNavigationMenuProps) {
|
||||
const { isLockable } = props;
|
||||
|
||||
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
|
||||
|
||||
const isViewLocked = isStringBoolean(searchParams.get('locked'));
|
||||
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
|
||||
|
||||
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
|
||||
interface EditorVisibilityOptions {
|
||||
isLockable?: boolean;
|
||||
}
|
||||
|
||||
export default function useViewEditor({ isLockable }: EditorVisibilityOptions) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const isViewLocked = useMemo(() => {
|
||||
if (!isLockable) {
|
||||
return false;
|
||||
}
|
||||
return isStringBoolean(searchParams.get('locked'));
|
||||
}, [isLockable, searchParams]);
|
||||
|
||||
return { showEditFormDrawer, isViewLocked };
|
||||
}
|
||||
@@ -119,7 +119,7 @@ function MultiOption(props: EditFormMultiOptionProps) {
|
||||
<MenuButton as={Button} variant='ontime-subtle-white' position='relative' width='fit-content' fontWeight={400}>
|
||||
{paramField.title} <IoChevronDown style={{ display: 'inline' }} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuList overflow='auto' maxHeight='200px'>
|
||||
<MenuOptionGroup
|
||||
type='checkbox'
|
||||
value={paramState}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { resolvePath } from 'react-router-dom';
|
||||
|
||||
import { generateUrlFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
|
||||
|
||||
describe('A preset fails if incorrect', () => {
|
||||
const testsToFail = [
|
||||
// no empty
|
||||
'',
|
||||
// no https, http or www
|
||||
'https://www.test.com',
|
||||
'http://www.test.com',
|
||||
'www.test.com',
|
||||
// no hostname
|
||||
'localhost/test',
|
||||
'127.0.0.1/test',
|
||||
'0.0.0.0/test',
|
||||
// no editor
|
||||
'editor',
|
||||
'editor?test',
|
||||
];
|
||||
|
||||
testsToFail.forEach((t) =>
|
||||
it(`${t}`, () => {
|
||||
expect(validateUrlPresetPath(t).isValid).toBeFalsy();
|
||||
}),
|
||||
);
|
||||
});
|
||||
describe('generateUrlFromPreset and getRouteFromPreset function', () => {
|
||||
test('generate the expected url from an alias', () => {
|
||||
const testData = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
url: '/timer?user=guest&alias=demopage',
|
||||
},
|
||||
];
|
||||
|
||||
expect(generateUrlFromPreset(testData[0])).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate the url to redirect to when the current URL is just the alias', () => {
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
},
|
||||
];
|
||||
// let current location be the alias
|
||||
const location = resolvePath(presets[0].alias);
|
||||
|
||||
const expected = [
|
||||
{
|
||||
url: '/timer?user=guest&alias=demopage',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getRouteFromPreset(location, presets, null)).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate the url to redirect to when the current URL the same url but with a change of params', () => {
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
},
|
||||
];
|
||||
// let current location be the actual url with alias attached to it
|
||||
const location = resolvePath(presets[0].pathAndParams);
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
urlSearchParams.append('alias', presets[0].alias); //
|
||||
|
||||
// update current alias with extra param
|
||||
presets[0].pathAndParams += '&eventId=674';
|
||||
const expected = [
|
||||
{
|
||||
url: '/timer?user=guest&eventId=674&alias=demopage',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getRouteFromPreset(location, presets, urlSearchParams)).toStrictEqual(expected[0].url);
|
||||
});
|
||||
test('generate no url to redirect to when the current URL the same url', () => {
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
},
|
||||
];
|
||||
// let current location be the actual url with alias attached to it
|
||||
const location = resolvePath(presets[0].pathAndParams);
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
urlSearchParams.append('alias', presets[0].alias); //
|
||||
|
||||
expect(getRouteFromPreset(location, presets, urlSearchParams)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { resolvePath } from 'react-router-dom';
|
||||
|
||||
import { arePathsEquivalent, generatePathFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
|
||||
|
||||
describe('validateUrlPresetPaths()', () => {
|
||||
test.each([
|
||||
// no empty
|
||||
'',
|
||||
// no https, http or www
|
||||
'https://www.test.com',
|
||||
'http://www.test.com',
|
||||
'www.test.com',
|
||||
// no hostname
|
||||
'localhost/test',
|
||||
'127.0.0.1/test',
|
||||
'0.0.0.0/test',
|
||||
// no editor
|
||||
'editor',
|
||||
'editor?test',
|
||||
])('flags known edge cases: %s', (t) => {
|
||||
expect(validateUrlPresetPath(t).isValid).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRouteFromPreset()', () => {
|
||||
const presets = [
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
},
|
||||
];
|
||||
|
||||
it('checks if the current location matches an enabled preset', () => {
|
||||
// we make the current location be the alias
|
||||
const location = resolvePath('demopage');
|
||||
expect(getRouteFromPreset(location, presets)).toStrictEqual('timer?user=guest&alias=demopage');
|
||||
});
|
||||
|
||||
it('returns null if the current location is the exact match of an unwrapped alias', () => {
|
||||
// we make the current location be the alias
|
||||
const location = resolvePath('/timer?user=guest&alias=demopage');
|
||||
expect(getRouteFromPreset(location, presets)).toEqual(null);
|
||||
});
|
||||
|
||||
it('returns a new destination if the current location is an out-of-date unwrapped alias', () => {
|
||||
// we make the current location be the alias
|
||||
const location = resolvePath('/timer?user=admin&alias=demopage');
|
||||
expect(getRouteFromPreset(location, presets)).toEqual('timer?user=guest&alias=demopage');
|
||||
});
|
||||
|
||||
it('checks if the current location contains an unwrapped preset', () => {
|
||||
// we make the current location be the alias
|
||||
const location = resolvePath('/timer?user=guest&alias=demopage');
|
||||
expect(getRouteFromPreset(location, presets)).toEqual(null);
|
||||
});
|
||||
|
||||
it('ignores a location that has no presets', () => {
|
||||
// we make the current location be the alias
|
||||
const location = resolvePath('/unknown');
|
||||
expect(getRouteFromPreset(location, presets)).toEqual(null);
|
||||
});
|
||||
|
||||
describe('handle url sharing edge cases', () => {
|
||||
it('finds the correct preset when the url contains extra arguments', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('appends the feature params to the alias', () => {
|
||||
const location = resolvePath('/demopage?locked=true&token=123');
|
||||
expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123')
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePathFromPreset()', () => {
|
||||
test.each([
|
||||
['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
|
||||
['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
|
||||
])('generates a path from a preset: %s', (path, alias, expected) => {
|
||||
expect(generatePathFromPreset(path, alias, null, null)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('appends the feature params to the alias', () => {
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('arePathsEquivalent()', () => {
|
||||
it("checks whether the paths match", () => {
|
||||
expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer', 'timer')).toBeTruthy();
|
||||
expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy();
|
||||
})
|
||||
|
||||
it("checks whether the params match", () => {
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy();
|
||||
})
|
||||
|
||||
it("considers edge cases for the url sharing feature", () => {
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy();
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { Location, resolvePath } from 'react-router-dom';
|
||||
import { Path, resolvePath } from 'react-router-dom';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Validates a preset against defined parameters
|
||||
* @param {string} preset
|
||||
* @returns {{message: string, isValid: boolean}}
|
||||
* Used in the context of form validation
|
||||
*/
|
||||
export const validateUrlPresetPath = (preset: string): { message: string; isValid: boolean } => {
|
||||
export function validateUrlPresetPath(preset: string): { message: string; isValid: boolean } {
|
||||
if (preset === '' || preset == null) {
|
||||
return { isValid: false, message: 'Path cannot be empty' };
|
||||
}
|
||||
@@ -26,7 +24,7 @@ export const validateUrlPresetPath = (preset: string): { message: string; isVali
|
||||
}
|
||||
|
||||
return { isValid: true, message: 'ok' };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility removes trailing slash from a string
|
||||
@@ -36,48 +34,93 @@ function removeTrailingSlash(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL to send a preset to
|
||||
* @param location
|
||||
* @param data
|
||||
* @param searchParams
|
||||
* Checks whether the current location corresponds to a preset and returns the new path if necessary
|
||||
*/
|
||||
export const getRouteFromPreset = (location: Location, data: URLPreset[], searchParams: URLSearchParams) => {
|
||||
export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): string | null {
|
||||
// current url is the pathname without the leading slash
|
||||
const currentURL = location.pathname.substring(1);
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
// we need to check if the whole url here is an alias, so we can redirect
|
||||
const foundPreset = data.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
|
||||
// check if we have token or locked in the search params
|
||||
const locked = searchParams.get('locked');
|
||||
const token = searchParams.get('token');
|
||||
|
||||
// we need to check if the whole url is an alias
|
||||
const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
|
||||
if (foundPreset) {
|
||||
return generateUrlFromPreset(foundPreset);
|
||||
// if so, we can redirect to the preset path
|
||||
return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token);
|
||||
}
|
||||
|
||||
// if the current url is not an alias, we check if the alias is in the search parameters
|
||||
const presetOnPage = searchParams.get('alias');
|
||||
for (const d of data) {
|
||||
if (presetOnPage) {
|
||||
// if the alias fits the preset on this page, but the URL is different, we redirect user to the new URL
|
||||
// if we have the same alias and its enabled and its not empty
|
||||
if (d.alias !== '' && d.enabled && d.alias === presetOnPage) {
|
||||
const newPath = resolvePath(d.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newPath.search);
|
||||
urlParams.set('alias', d.alias);
|
||||
// we confirm either the url parameters does not match or the url path doesnt
|
||||
if (!isEqual(urlParams, searchParams) || newPath.pathname !== location.pathname) {
|
||||
// we then redirect to the alias route, since the view listening to this alias has an outdated URL
|
||||
return `${newPath.pathname}?${urlParams}`;
|
||||
}
|
||||
if (!presetOnPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPath = `${location.pathname}${location.search}`.substring(1);
|
||||
|
||||
for (const preset of urlPresets) {
|
||||
// if the page has a known enabled alias, we check if we need to redirect
|
||||
if (preset.alias === presetOnPage && preset.enabled) {
|
||||
const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token);
|
||||
if (!arePathsEquivalent(currentPath, newPath)) {
|
||||
// if current path is out of date
|
||||
// return new path so we can redirect
|
||||
return newPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL from an preset
|
||||
* @param presetData
|
||||
* Handles generating a path and search parameters from a preset
|
||||
*/
|
||||
export const generateUrlFromPreset = (presetData: URLPreset) => {
|
||||
const newPresetPath = resolvePath(presetData.pathAndParams);
|
||||
const urlParams = new URLSearchParams(newPresetPath.search);
|
||||
urlParams.set('alias', presetData.alias);
|
||||
export function generatePathFromPreset(pathAndParams: string, alias: string, locked: string | null, token: string | null ): string {
|
||||
const path = resolvePath(pathAndParams);
|
||||
const searchParams = new URLSearchParams(path.search);
|
||||
|
||||
return `${newPresetPath.pathname}?${urlParams}`;
|
||||
};
|
||||
// save the alias so we have a reference to it being a preset and can update if necessary
|
||||
searchParams.set('alias', alias);
|
||||
|
||||
// maintain params from the URL search feature
|
||||
if (locked) {
|
||||
searchParams.set('locked', locked);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
searchParams.set('token', token);
|
||||
}
|
||||
|
||||
// return path concatenated without the leading slash
|
||||
return `${path.pathname}?${searchParams}`.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility checks if two paths are equivalent
|
||||
* Considers the edge cases for url sharing where a path may contain extra arguments from the alias
|
||||
* - token
|
||||
* - locked
|
||||
*/
|
||||
export function arePathsEquivalent(currentPath: string, newPath: string): boolean {
|
||||
const currentUrl = new URL(currentPath, document.location.origin);
|
||||
const newUrl = new URL(newPath, document.location.origin);
|
||||
|
||||
// check path
|
||||
if (currentUrl.pathname !== newUrl.pathname) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check search params
|
||||
// if the params match, we dont need further checks
|
||||
if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if there is no match, we check the edge cases for the url sharing feature
|
||||
currentUrl.searchParams.delete('token');
|
||||
currentUrl.searchParams.delete('locked');
|
||||
|
||||
return currentUrl.searchParams.toString() === newUrl.searchParams.toString();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { ComponentType, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import useUrlPresets from '../common/hooks-query/useUrlPresets';
|
||||
import { getRouteFromPreset } from '../common/utils/urlPresets';
|
||||
@@ -8,19 +8,19 @@ import { getRouteFromPreset } from '../common/utils/urlPresets';
|
||||
const withPreset = <P extends object>(Component: ComponentType<P>) => {
|
||||
return (props: Partial<P>) => {
|
||||
const { data } = useUrlPresets();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// navigate if is alias route
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const url = getRouteFromPreset(location, data, searchParams);
|
||||
// navigate to this route if its not empty
|
||||
if (url) {
|
||||
navigate(url);
|
||||
const destination = getRouteFromPreset(location, data);
|
||||
|
||||
// navigate to this destination if its not null
|
||||
if (destination) {
|
||||
navigate(destination);
|
||||
}
|
||||
}, [data, searchParams, navigate, location]);
|
||||
}, [data, navigate, location]);
|
||||
|
||||
return <Component {...(props as P)} />;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ $inner-padding: 1rem;
|
||||
.section {
|
||||
position: relative;
|
||||
margin-top: 2rem;
|
||||
max-width: 800px;
|
||||
max-width: 850px;
|
||||
}
|
||||
|
||||
.indent {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import TemplateInput from './template-input/TemplateInput';
|
||||
import { isAutomation, makeFieldList } from './automationUtils';
|
||||
|
||||
import style from './AutomationForm.module.scss';
|
||||
@@ -251,7 +252,7 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
{...register(`filters.${index}.value`)}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='<no value>'
|
||||
placeholder='<empty / no value>'
|
||||
autoComplete='off'
|
||||
/>
|
||||
</label>
|
||||
@@ -357,7 +358,7 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
</label>
|
||||
<label>
|
||||
Parameters
|
||||
<Input
|
||||
<TemplateInput
|
||||
{...register(`outputs.${index}.args`)}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
|
||||
@@ -27,12 +27,12 @@ export default function AutomationPanel({ location }: PanelBaseProps) {
|
||||
oscPortIn={data.oscPortIn}
|
||||
/>
|
||||
</div>
|
||||
<div ref={triggersRef}>
|
||||
<TriggersList triggers={data.triggers} automations={data.automations} />
|
||||
</div>
|
||||
<div ref={automationsRef}>
|
||||
<AutomationsList automations={data.automations} />
|
||||
</div>
|
||||
<div ref={triggersRef}>
|
||||
<TriggersList triggers={data.triggers} automations={data.automations} />
|
||||
</div>
|
||||
</Panel.Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,12 @@ const staticSelectProperties = [
|
||||
{ value: 'eventNow.colour', label: 'Colour' },
|
||||
];
|
||||
|
||||
const staticNextSelectProperties = [
|
||||
{ value: 'eventNow.id', label: 'Next ID' },
|
||||
{ value: 'eventNow.title', label: 'Next Title' },
|
||||
{ value: 'eventNow.cue', label: 'Next Cue' },
|
||||
];
|
||||
|
||||
type SelectableField = {
|
||||
value: string; // string encodes path in runtime state object
|
||||
label: string;
|
||||
@@ -48,6 +54,11 @@ export function makeFieldList(customFields: CustomFields): SelectableField[] {
|
||||
value: `eventNow.custom.${key}`,
|
||||
label: `Custom: ${label}`,
|
||||
})),
|
||||
...staticNextSelectProperties,
|
||||
...Object.entries(customFields).map(([key, { label }]) => ({
|
||||
value: `eventNext.custom.${key}`,
|
||||
label: `Next custom: ${label}`,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
background: $gray-1250;
|
||||
color: $ui-white;
|
||||
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
z-index: 5;
|
||||
padding-block: 0.25rem;
|
||||
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: $label-gray;
|
||||
|
||||
li {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
color: $ui-white;
|
||||
background: $blue-700;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { type InputProps, Input } from '@chakra-ui/react';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
|
||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||
|
||||
import { makeAutoCompleteList, matchRemaining, selectFromLastTemplate } from './templateInput.utils';
|
||||
|
||||
import style from './TemplateInput.module.scss';
|
||||
|
||||
interface TemplateInputProps extends InputProps {}
|
||||
|
||||
export default function TemplateInput(props: TemplateInputProps) {
|
||||
const { value, onChange, ...rest } = props;
|
||||
const { data } = useCustomFields();
|
||||
const ref = useClickOutside(() => setShowSuggestions(false));
|
||||
|
||||
const autocompleteList = useMemo(() => {
|
||||
return makeAutoCompleteList(data);
|
||||
}, [data]);
|
||||
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
|
||||
const updateSuggestions = (value: string) => {
|
||||
const template = selectFromLastTemplate(value);
|
||||
return autocompleteList.filter((suggestion) => suggestion.startsWith(template));
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
|
||||
if (event.target.value.endsWith('{{')) {
|
||||
setShowSuggestions(true);
|
||||
} else if (event.target.value === '' || event.target.value.endsWith('}}')) {
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
|
||||
if (showSuggestions) {
|
||||
const suggestions = updateSuggestions(event.target.value);
|
||||
setSuggestions(suggestions);
|
||||
}
|
||||
|
||||
onChange?.(event);
|
||||
};
|
||||
|
||||
const handleSuggestion = (value: string) => {
|
||||
setInputValue((prev) => {
|
||||
const remaining = matchRemaining(prev as string, value);
|
||||
return prev + remaining;
|
||||
});
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.wrapper} ref={ref}>
|
||||
<Input {...rest} value={inputValue} onChange={handleInputChange} autoComplete='off' autoCorrect='off' />
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<ul className={style.suggestions}>
|
||||
{suggestions.map((suggestion) => (
|
||||
<li key={suggestion} onClick={() => handleSuggestion(suggestion)}>
|
||||
{suggestion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { matchRemaining } from '../templateInput.utils';
|
||||
|
||||
describe('matchRemaining()', () => {
|
||||
it('should return a partial string needed for autocomplete', () => {
|
||||
expect(matchRemaining('te', 'test')).toBe('st');
|
||||
expect(matchRemaining('{{{hum', '{{human}}')).toBe('an}}');
|
||||
expect(matchRemaining('send {', '{{human}}')).toBe('{human}}');
|
||||
|
||||
// we should be able to match the following
|
||||
// however, the current implementation only needs to deal with strings that start with {{
|
||||
// expect(matchRemaining('{', '{{human}}')).toBe('{human}}');
|
||||
// expect(matchRemaining('{{', '{{human}}')).toBe('human}}');
|
||||
});
|
||||
|
||||
it('should return an empty string if there are no matches or if it is complete', () => {
|
||||
expect(matchRemaining('test', 'another')).toBe('');
|
||||
expect(matchRemaining('test', 'test')).toBe('');
|
||||
});
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { CustomFields } from 'ontime-types';
|
||||
|
||||
const humanConstants = [
|
||||
'{{human.clock}}',
|
||||
'{{human.duration}}',
|
||||
'{{human.expectedEnd}}',
|
||||
'{{human.runningTimer}}',
|
||||
'{{human.elapsedTime}}',
|
||||
'{{human.startedAt}}',
|
||||
];
|
||||
|
||||
const staticAutocompleteOptions = [
|
||||
'{{clock}}',
|
||||
'{{timer.addedTime}}',
|
||||
'{{timer.current}}',
|
||||
'{{timer.duration}}',
|
||||
'{{timer.elapsed}}',
|
||||
'{{timer.expectedFinish}}',
|
||||
'{{timer.finishedAt}}',
|
||||
'{{timer.secondaryTimer}}',
|
||||
'{{timer.startedAt}}',
|
||||
'{{runtime.selectedEventIndex}}',
|
||||
'{{runtime.numEvents}}',
|
||||
'{{runtime.offset}}',
|
||||
'{{runtime.plannedStart}}',
|
||||
'{{runtime.plannedEnd}}',
|
||||
'{{runtime.actualStart}}',
|
||||
'{{runtime.expectedEnd}}',
|
||||
'{{currentBlock.block}}',
|
||||
'{{currentBlock.startedAt}}',
|
||||
];
|
||||
|
||||
const eventStaticPropertiesNow = [
|
||||
'{{eventNow.id}}',
|
||||
'{{eventNow.cue}}',
|
||||
'{{eventNow.title}}',
|
||||
'{{eventNow.note}}',
|
||||
'{{eventNow.timeStart}}',
|
||||
'{{eventNow.timeEnd}}',
|
||||
'{{eventNow.duration}}',
|
||||
'{{eventNow.isPublic}}',
|
||||
'{{eventNow.colour}}',
|
||||
'{{eventNow.delay}}',
|
||||
];
|
||||
|
||||
const eventStaticPropertiesNext = [
|
||||
'{{eventNext.id}}',
|
||||
'{{eventNext.cue}}',
|
||||
'{{eventNext.title}}',
|
||||
'{{eventNext.note}}',
|
||||
'{{eventNext.timeStart}}',
|
||||
'{{eventNext.timeEnd}}',
|
||||
'{{eventNext.duration}}',
|
||||
'{{eventNext.isPublic}}',
|
||||
'{{eventNext.colour}}',
|
||||
'{{eventNext.delay}}',
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a it of possible autocomplete suggestions
|
||||
* Based on RuntimeState
|
||||
* Appends the human readable variants to it
|
||||
* It is manually kept in sync with the automation parseTemplate functions
|
||||
*/
|
||||
export function makeAutoCompleteList(customFields: CustomFields): string[] {
|
||||
return [
|
||||
...humanConstants,
|
||||
...staticAutocompleteOptions,
|
||||
...eventStaticPropertiesNow,
|
||||
...Object.entries(customFields).map(([key]) => `{{eventNow.custom.${key}}}`),
|
||||
...eventStaticPropertiesNext,
|
||||
...Object.entries(customFields).map(([key]) => `{{eventNext.custom.${key}}}`),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the partial string b needed to autocomplete string a
|
||||
* @example matchRemaining('te', 'test') -> 'st'
|
||||
* @example matchRemaining('{{', '{{human}}') -> 'man}}'
|
||||
*/
|
||||
export function matchRemaining(a: string, b: string) {
|
||||
if (a === b) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
const searchString = b.substring(0, i + 1);
|
||||
if (a.endsWith(searchString)) {
|
||||
return b.substring(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the last starting template in a string
|
||||
*/
|
||||
export function selectFromLastTemplate(text: string) {
|
||||
const lastBraceIndex = text.lastIndexOf('{{');
|
||||
if (lastBraceIndex !== -1) {
|
||||
return text.slice(lastBraceIndex);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import GeneralPinInput from './GeneralPinInput';
|
||||
@@ -25,6 +26,7 @@ export default function GeneralPanelForm() {
|
||||
setError,
|
||||
formState: { isSubmitting, isDirty, isValid, errors },
|
||||
} = useForm<Settings>({
|
||||
mode: 'onChange',
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
@@ -94,7 +96,11 @@ export default function GeneralPanelForm() {
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Ontime server port'
|
||||
description='Port ontime server listens in. Defaults to 4001 (needs app restart)'
|
||||
description={
|
||||
isOntimeCloud
|
||||
? 'Server port disabled for Ontime Cloud'
|
||||
: 'Port ontime server listens in. Defaults to 4001 (needs app restart)'
|
||||
}
|
||||
error={errors.serverPort?.message}
|
||||
/>
|
||||
<Input
|
||||
@@ -104,6 +110,7 @@ export default function GeneralPanelForm() {
|
||||
variant='ontime-filled'
|
||||
maxLength={5}
|
||||
width='75px'
|
||||
isDisabled={isOntimeCloud}
|
||||
{...register('serverPort', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||
|
||||
@@ -4,6 +4,8 @@ import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/rea
|
||||
import { IoEyeOutline } from '@react-icons/all-files/io5/IoEyeOutline';
|
||||
import { Settings } from 'ontime-types';
|
||||
|
||||
import { isAlphanumeric } from '../../../../common/utils/regex';
|
||||
|
||||
interface GeneralPinInputProps {
|
||||
register: UseFormRegister<Settings>;
|
||||
formName: keyof Settings;
|
||||
@@ -13,14 +15,18 @@ interface GeneralPinInputProps {
|
||||
export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInputProps>) {
|
||||
const { register, formName, isDisabled } = props;
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' width='100px'>
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
maxLength={4}
|
||||
{...register(formName)}
|
||||
{...register(formName, {
|
||||
pattern: {
|
||||
value: isAlphanumeric,
|
||||
message: 'Only alphanumeric characters are allowed',
|
||||
},
|
||||
})}
|
||||
placeholder='-'
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
@@ -76,6 +76,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
||||
const handleConnect = async () => {
|
||||
if (!file) return;
|
||||
if (!sheetId) return;
|
||||
patchStepData({ worksheet: { available: false, error: '' } });
|
||||
|
||||
setLoading('connect');
|
||||
const result = await connect(file, sheetId);
|
||||
|
||||
@@ -51,8 +51,8 @@ const staticOptions = [
|
||||
label: 'Automation',
|
||||
secondary: [
|
||||
{ id: 'automation__settings', label: 'Automation settings' },
|
||||
{ id: 'automation__triggers', label: 'Manage triggers' },
|
||||
{ id: 'automation__automations', label: 'Manage automations' },
|
||||
{ id: 'automation__triggers', label: 'Manage triggers' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useElectronListener } from '../../common/hooks/useElectronEvent';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import AppSettings from '../app-settings/AppSettings';
|
||||
@@ -58,7 +58,7 @@ export default function Editor() {
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<WelcomePlacement />
|
||||
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
|
||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||
<NavigationMenu isOpen={isMenuOpen} onClose={onClose} />
|
||||
<EditorOverview>
|
||||
<IconButton
|
||||
aria-label='Toggle navigation'
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import FloatingNavigation from '../../common/components/navigation-menu/FloatingNavigation';
|
||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||
|
||||
import Operator from './Operator';
|
||||
|
||||
export default function OperatorExport() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const toggleMenu = isOpen ? onClose : onOpen;
|
||||
|
||||
return (
|
||||
<ProtectRoute permission='operator'>
|
||||
<FloatingNavigation toggleMenu={toggleMenu} toggleSettings={showEditFormDrawer} />
|
||||
<ProductionNavigationMenu isMenuOpen={isOpen} onMenuClose={onClose} />
|
||||
<ViewNavigationMenu isLockable />
|
||||
<Operator />
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
&.running {
|
||||
border-top: 1px solid $gray-1300;
|
||||
background-color: $active-red;
|
||||
background-color: var(--operator-running-bg-override, $active-red);
|
||||
}
|
||||
|
||||
&.past {
|
||||
@@ -92,12 +92,14 @@
|
||||
|
||||
.fields {
|
||||
grid-area: fields;
|
||||
font-size: 1.25rem;
|
||||
font-size: var(--operator-customfield-font-size-override, 1.25rem);
|
||||
font-weight: 400;
|
||||
color: $ui-black;
|
||||
margin: 0.25rem 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
row-gap: 0.25em;
|
||||
|
||||
.field {
|
||||
font-weight: 600;
|
||||
@@ -112,10 +114,11 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
display: inline-flex;
|
||||
padding-inline: 0.25rem;
|
||||
background-color: $gray-1250;
|
||||
color: $ui-white;
|
||||
margin-right: 1rem;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewNavigationMenu />
|
||||
<ViewNavigationMenu isLockable />
|
||||
<Component
|
||||
{...props}
|
||||
auxTimer={auxtimer1}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
|
||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||
import useViewEditor from '../../common/components/navigation-menu/useViewEditor';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
@@ -25,7 +25,7 @@ export default function CuesheetPage() {
|
||||
// TODO: can we use the normalised rundown for the table?
|
||||
const { data: flatRundown } = useFlatRundown();
|
||||
const { data: customFields } = useCustomFields();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable: true });
|
||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
||||
const [eventId, setEventId] = useState<string | null>(null);
|
||||
@@ -34,12 +34,6 @@ export default function CuesheetPage() {
|
||||
|
||||
useWindowTitle('Cuesheet');
|
||||
|
||||
/** Handles showing the view params edit drawer */
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
/**
|
||||
* Handles setting the edit modal target and visibility
|
||||
*/
|
||||
@@ -69,7 +63,7 @@ export default function CuesheetPage() {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||
<NavigationMenu isOpen={isMenuOpen} onClose={onClose} />
|
||||
<ViewParamsEditor viewOptions={cuesheetOptions} />
|
||||
<CuesheetOverview>
|
||||
<IconButton
|
||||
@@ -78,6 +72,7 @@ export default function CuesheetPage() {
|
||||
size='lg'
|
||||
icon={<IoApps />}
|
||||
onClick={onOpen}
|
||||
isDisabled={isViewLocked}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label='Toggle settings'
|
||||
@@ -85,6 +80,7 @@ export default function CuesheetPage() {
|
||||
size='lg'
|
||||
icon={<IoSettingsOutline />}
|
||||
onClick={showEditFormDrawer}
|
||||
isDisabled={isViewLocked}
|
||||
/>
|
||||
</CuesheetOverview>
|
||||
<CuesheetProgress />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-electron",
|
||||
"version": "3.11.0-beta.1",
|
||||
"version": "3.11.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "3.11.0-beta.1",
|
||||
"version": "3.11.0",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
@@ -19,7 +19,7 @@
|
||||
"lowdb": "^7.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"ontime-utils": "workspace:*",
|
||||
"osc-min": "^2.1.1",
|
||||
"osc-min": "2.1.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"steno": "^4.0.2",
|
||||
"ws": "^8.18.0",
|
||||
@@ -30,7 +30,6 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "catalog:",
|
||||
"@types/node-osc": "^6.0.3",
|
||||
"@types/websocket": "^1.0.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
|
||||
@@ -20,17 +20,13 @@ class OscServer implements IAdapter {
|
||||
logger.info(LogOrigin.Rx, `OSC: Starting server on port ${port}`);
|
||||
this.udpSocket = dgram.createSocket('udp4');
|
||||
this.udpSocket.on('error', (error) => logger.error(LogOrigin.Rx, `OSC IN: ${error}`));
|
||||
this.udpSocket.on('message', (buf: ArrayBuffer) => {
|
||||
this.udpSocket.on('message', (buf) => {
|
||||
// message should look like /ontime/{command}/{params?} {args} where
|
||||
// ontime: fixed message for app
|
||||
// command: command to be called
|
||||
// params: used to create a nested object to patch with
|
||||
// args: extra data, only used on some API entries
|
||||
|
||||
/**
|
||||
* TODO: remove this type casting when mergend in deleration file
|
||||
* https://github.com/DefinitelyTyped/DefinitelyTyped/pull/71659
|
||||
*/
|
||||
const msg = fromBuffer(buf);
|
||||
if (msg.oscType === 'bundle') {
|
||||
//TODO: manage bundles
|
||||
|
||||
@@ -33,11 +33,7 @@ function preparePayload(output: OSCOutput, state: RuntimeState): OscPacketInput
|
||||
function emit(targetIP: string, targetPort: number, packet: OscPacketInput) {
|
||||
logger.info(LogOrigin.Tx, `Sending OSC: ${targetIP}:${targetPort}`);
|
||||
|
||||
/**
|
||||
* TODO: remove this type casting when change is merged
|
||||
* https://github.com/DefinitelyTyped/DefinitelyTyped/pull/71659
|
||||
*/
|
||||
const buffer = oscPacketToBuffer(packet) as unknown as Uint8Array;
|
||||
const buffer = oscPacketToBuffer(packet);
|
||||
udpClient.send(buffer, 0, buffer.byteLength, targetPort, targetIP, (error) => {
|
||||
if (error) {
|
||||
logger.warning(LogOrigin.Tx, `Failed sending OSC: ${error}`);
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
||||
*/
|
||||
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||
try {
|
||||
const newFileName = await projectService.createProject(req.body.filename, {
|
||||
const newFileName = await projectService.createProject(req.body.filename || 'untitled', {
|
||||
project: {
|
||||
title: req.body?.title ?? '',
|
||||
description: req.body?.description ?? '',
|
||||
@@ -75,7 +75,7 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
||||
*/
|
||||
export async function quickProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||
try {
|
||||
const filename = await projectService.createProject(req.body.project.title, req.body);
|
||||
const filename = await projectService.createProject(req.body.project.title || 'untitled', req.body);
|
||||
res.status(200).send({
|
||||
filename,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||
* @description Validates request for a new project.
|
||||
*/
|
||||
export const validateNewProject = [
|
||||
body('filename').optional().isString().trim(),
|
||||
body('title').optional().isString().trim(),
|
||||
body('description').optional().isString().trim(),
|
||||
body('publicUrl').optional().isString().trim(),
|
||||
@@ -132,8 +133,7 @@ export const validateFilenameParam = [
|
||||
.customSanitizer((input: string) => sanitize(input))
|
||||
.withMessage('Failed to sanitize the filename')
|
||||
.notEmpty()
|
||||
.withMessage('Filename was empty or contained only invalid characters')
|
||||
.customSanitizer((input: string) => ensureJsonExtension(input)),
|
||||
.withMessage('Filename was empty or contained only invalid characters'),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
appendToName,
|
||||
dockerSafeRename,
|
||||
ensureDirectory,
|
||||
ensureJsonExtension,
|
||||
generateUniqueFileName,
|
||||
getFileNameFromPath,
|
||||
removeFileExtension,
|
||||
@@ -20,6 +21,7 @@ import { parseRundown } from '../../utils/parserFunctions.js';
|
||||
import { demoDb } from '../../models/demoProject.js';
|
||||
import { config } from '../../setup/config.js';
|
||||
import { getDataProvider, initPersistence } from '../../classes/data-provider/DataProvider.js';
|
||||
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
|
||||
|
||||
import { initRundown } from '../rundown-service/RundownService.js';
|
||||
import {
|
||||
@@ -37,7 +39,6 @@ import {
|
||||
moveCorruptFile,
|
||||
parseJsonFile,
|
||||
} from './projectServiceUtils.js';
|
||||
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
|
||||
|
||||
// init dependencies
|
||||
init();
|
||||
@@ -257,7 +258,8 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
export async function createProject(filename: string, initialData: Partial<DatabaseModel>) {
|
||||
const data = safeMerge(dbModel, initialData);
|
||||
|
||||
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, filename);
|
||||
const fileNameWithExtension = ensureJsonExtension(filename);
|
||||
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, fileNameWithExtension);
|
||||
const newFile = getPathToProject(uniqueFileName);
|
||||
|
||||
// change LowDB to point to new file
|
||||
|
||||
@@ -208,6 +208,15 @@ async function verifySheet(
|
||||
});
|
||||
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
|
||||
} catch (error) {
|
||||
// attempt to catch errors caused by importing xlsx
|
||||
if (
|
||||
error.code === 400 &&
|
||||
Array.isArray(error.errors) &&
|
||||
error.errors[0].reason === 'failedPrecondition' &&
|
||||
error.errors[0].message === 'This operation is not supported for this document'
|
||||
) {
|
||||
throw new Error('Cannot read the linked file as a Google Sheet. It may be an .xlsx file instead.');
|
||||
}
|
||||
const errorMessage = getErrorMessage(error);
|
||||
throw new Error(`Failed to verify sheet: ${errorMessage}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { PlayableEvent, Playback, TimerPhase } from 'ontime-types';
|
||||
import { OntimeRundown, PlayableEvent, Playback, SupportedEvent, TimerPhase } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import { type RuntimeState, addTime, clear, getState, load, pause, roll, start, stop } from '../runtimeState.js';
|
||||
import {
|
||||
type RuntimeState,
|
||||
addTime,
|
||||
clear,
|
||||
getState,
|
||||
load,
|
||||
loadBlock,
|
||||
pause,
|
||||
roll,
|
||||
start,
|
||||
stop,
|
||||
} from '../runtimeState.js';
|
||||
import { initRundown } from '../../services/rundown-service/RundownService.js';
|
||||
|
||||
const mockEvent = {
|
||||
@@ -328,3 +339,125 @@ describe('roll mode', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadBlock', () => {
|
||||
test('from no-block to a block will clear startedAt', () => {
|
||||
const rundown = [
|
||||
{ id: '0', type: SupportedEvent.Event },
|
||||
{ id: '1', type: SupportedEvent.Block },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
] as OntimeRundown;
|
||||
|
||||
const state = {
|
||||
currentBlock: {
|
||||
block: null,
|
||||
startedAt: 123,
|
||||
},
|
||||
eventNow: rundown[2],
|
||||
} as RuntimeState;
|
||||
|
||||
loadBlock(rundown, state);
|
||||
|
||||
expect(state).toMatchObject({
|
||||
currentBlock: { block: rundown[1], startedAt: null },
|
||||
eventNow: rundown[2],
|
||||
});
|
||||
});
|
||||
|
||||
test('from block to a different block will clear startedAt', () => {
|
||||
const rundown = [
|
||||
{ id: '0', type: SupportedEvent.Event },
|
||||
{ id: '1', type: SupportedEvent.Block },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
] as OntimeRundown;
|
||||
|
||||
const state = {
|
||||
currentBlock: {
|
||||
block: rundown[1],
|
||||
startedAt: 123,
|
||||
},
|
||||
eventNow: rundown[4],
|
||||
} as RuntimeState;
|
||||
|
||||
loadBlock(rundown, state);
|
||||
|
||||
expect(state).toMatchObject({
|
||||
currentBlock: { block: rundown[3], startedAt: null },
|
||||
eventNow: rundown[4],
|
||||
});
|
||||
});
|
||||
|
||||
test('from block to a no-block will clear startedAt', () => {
|
||||
const rundown = [
|
||||
{ id: '0', type: SupportedEvent.Event },
|
||||
{ id: '1', type: SupportedEvent.Block },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
] as OntimeRundown;
|
||||
|
||||
const state = {
|
||||
currentBlock: {
|
||||
block: rundown[1],
|
||||
startedAt: 123,
|
||||
},
|
||||
eventNow: rundown[0],
|
||||
} as RuntimeState;
|
||||
|
||||
loadBlock(rundown, state);
|
||||
|
||||
expect(state).toMatchObject({
|
||||
currentBlock: { block: null, startedAt: null },
|
||||
eventNow: rundown[0],
|
||||
});
|
||||
});
|
||||
|
||||
test('from block to same block will keep startedAt', () => {
|
||||
const rundown = [
|
||||
{ id: '0', type: SupportedEvent.Block },
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
] as OntimeRundown;
|
||||
|
||||
const state = {
|
||||
currentBlock: {
|
||||
block: rundown[0],
|
||||
startedAt: 123,
|
||||
},
|
||||
eventNow: rundown[2],
|
||||
} as RuntimeState;
|
||||
|
||||
loadBlock(rundown, state);
|
||||
|
||||
expect(state).toMatchObject({
|
||||
currentBlock: { block: rundown[0], startedAt: 123 },
|
||||
eventNow: rundown[2],
|
||||
});
|
||||
});
|
||||
|
||||
test('from no-block to no-block will keep startedAt', () => {
|
||||
const rundown = [
|
||||
{ id: '0', type: SupportedEvent.Event },
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
] as OntimeRundown;
|
||||
|
||||
const state = {
|
||||
currentBlock: {
|
||||
block: null,
|
||||
startedAt: 123,
|
||||
},
|
||||
eventNow: rundown[0],
|
||||
} as RuntimeState;
|
||||
|
||||
loadBlock(rundown, state);
|
||||
|
||||
expect(state).toMatchObject({
|
||||
currentBlock: { block: null, startedAt: 123 },
|
||||
eventNow: rundown[0],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
Runtime,
|
||||
runtimeStorePlaceholder,
|
||||
TimerPhase,
|
||||
TimerState,
|
||||
} from 'ontime-types';
|
||||
@@ -32,29 +33,6 @@ import {
|
||||
import { timerConfig } from '../config/config.js';
|
||||
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null, // changes if rundown changes or we load a new event
|
||||
numEvents: 0, // change initiated by user
|
||||
offset: 0, // changes at runtime
|
||||
plannedStart: 0, // only changes if event changes
|
||||
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
||||
actualStart: null, // set once we start the timer
|
||||
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
||||
} as const;
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
addedTime: 0,
|
||||
current: null, // changes on every update
|
||||
duration: null, // only changes if event changes
|
||||
elapsed: null, // changes on every update
|
||||
expectedFinish: null, // change can only be initiated by user, can roll over midnight
|
||||
finishedAt: null, // can change on update or user action
|
||||
phase: TimerPhase.None, // can change on update or user action
|
||||
playback: Playback.Stop, // change initiated by user
|
||||
secondaryTimer: null, // change on every update
|
||||
startedAt: null, // change can only be initiated by user
|
||||
} as const;
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
eventNow: PlayableEvent | null;
|
||||
@@ -75,16 +53,13 @@ export type RuntimeState = {
|
||||
|
||||
const runtimeState: RuntimeState = {
|
||||
clock: clock.timeNow(),
|
||||
currentBlock: {
|
||||
block: null,
|
||||
startedAt: null,
|
||||
},
|
||||
currentBlock: { ...runtimeStorePlaceholder.currentBlock },
|
||||
eventNow: null,
|
||||
publicEventNow: null,
|
||||
eventNext: null,
|
||||
publicEventNext: null,
|
||||
runtime: { ...initialRuntime },
|
||||
timer: { ...initialTimer },
|
||||
runtime: { ...runtimeStorePlaceholder.runtime },
|
||||
timer: { ...runtimeStorePlaceholder.timer },
|
||||
_timer: {
|
||||
forceFinish: null,
|
||||
totalDelay: 0,
|
||||
@@ -124,7 +99,7 @@ export function clear() {
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = clock.timeNow();
|
||||
runtimeState.timer = { ...initialTimer };
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
runtimeState._timer.forceFinish = null;
|
||||
@@ -492,6 +467,7 @@ export type UpdateResult = {
|
||||
|
||||
export function update(): UpdateResult {
|
||||
// 0. there are some things we always do
|
||||
const previousClock = runtimeState.clock;
|
||||
runtimeState.clock = clock.timeNow(); // we update the clock on every update call
|
||||
|
||||
// 1. is playback idle?
|
||||
@@ -501,7 +477,8 @@ export function update(): UpdateResult {
|
||||
|
||||
// 2. are we waiting to roll?
|
||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||
return updateIfWaitingToRoll();
|
||||
const hasCrossedMidnight = previousClock > runtimeState.clock;
|
||||
return updateIfWaitingToRoll(hasCrossedMidnight);
|
||||
}
|
||||
|
||||
// 3. at this point we know that we are playing an event
|
||||
@@ -543,16 +520,24 @@ export function update(): UpdateResult {
|
||||
return { hasTimerFinished: false, hasSecondaryTimerFinished: false };
|
||||
}
|
||||
|
||||
function updateIfWaitingToRoll() {
|
||||
function updateIfWaitingToRoll(hasCrossedMidnight: boolean) {
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (runtimeState.eventNow === null || runtimeState._timer.secondaryTarget === null) {
|
||||
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
|
||||
}
|
||||
}
|
||||
//account for offset
|
||||
|
||||
// account for offset
|
||||
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
|
||||
if (hasCrossedMidnight) {
|
||||
// if we crossed midnight, we need to update the target
|
||||
// this is the same logic from the roll function
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
|
||||
}
|
||||
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0 };
|
||||
}
|
||||
@@ -664,6 +649,11 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString
|
||||
|
||||
// there is something to run, load event
|
||||
|
||||
// update runtime
|
||||
if (runtimeState.currentBlock.startedAt === null) {
|
||||
runtimeState.currentBlock.startedAt = runtimeState.clock;
|
||||
}
|
||||
|
||||
// event will finish on time
|
||||
// account for event that finishes the day after
|
||||
const endTime =
|
||||
@@ -686,26 +676,25 @@ export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString
|
||||
return { eventId: runtimeState.eventNow.id, didStart: true };
|
||||
}
|
||||
|
||||
function loadBlock(rundown: OntimeRundown) {
|
||||
if (runtimeState.eventNow === null) {
|
||||
/**
|
||||
* handle block loading, not for use outside of runtimeState
|
||||
* @param rundown
|
||||
*/
|
||||
export function loadBlock(rundown: OntimeRundown, state = runtimeState) {
|
||||
if (state.eventNow === null) {
|
||||
// we need a loaded event to have a block
|
||||
runtimeState.currentBlock.block = null;
|
||||
runtimeState.currentBlock.startedAt = null;
|
||||
state.currentBlock.block = null;
|
||||
state.currentBlock.startedAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const newCurrentBlock = getPreviousBlock(rundown, runtimeState.eventNow.id);
|
||||
|
||||
// test all block change posibiletys
|
||||
const formNoBlockToBlock = runtimeState.currentBlock.block === null && newCurrentBlock !== null;
|
||||
const formBlockToNoBlock = runtimeState.currentBlock.block !== null && newCurrentBlock === null;
|
||||
const formBlockToNewBlock = runtimeState.currentBlock.block?.id !== newCurrentBlock?.id;
|
||||
const newCurrentBlock = getPreviousBlock(rundown, state.eventNow.id);
|
||||
|
||||
// update time only if the block has changed
|
||||
if (formNoBlockToBlock || formBlockToNoBlock || formBlockToNewBlock) {
|
||||
runtimeState.currentBlock.startedAt = null;
|
||||
if (state.currentBlock.block?.id !== newCurrentBlock?.id) {
|
||||
state.currentBlock.startedAt = null;
|
||||
}
|
||||
|
||||
// update the block anyway
|
||||
runtimeState.currentBlock.block = newCurrentBlock === null ? null : { ...newCurrentBlock };
|
||||
state.currentBlock.block = newCurrentBlock === null ? null : { ...newCurrentBlock };
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { stringToOSCArgs } from '../oscArgParser.js';
|
||||
|
||||
describe('test stringToOSCArgs()', () => {
|
||||
it('all types', () => {
|
||||
const test = 'test 1111 0.1111 TRUE FALSE';
|
||||
const expected = [
|
||||
{ type: 'string', value: 'test' },
|
||||
{ type: 'integer', value: 1111 },
|
||||
{ type: 'float', value: 0.1111 },
|
||||
{ type: 'T', value: true },
|
||||
{ type: 'F', value: false },
|
||||
];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('empty is nothing', () => {
|
||||
const test = undefined;
|
||||
const expected: any[] = [];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('empty is nothing', () => {
|
||||
const test = '';
|
||||
const expected: any[] = [];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('1 space is nothing', () => {
|
||||
const test = ' ';
|
||||
const expected: any[] = [];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('keep other types in strings', () => {
|
||||
const test = 'test "1111" "0.1111" "TRUE" "FALSE"';
|
||||
const expected = [
|
||||
{ type: 'string', value: 'test' },
|
||||
{ type: 'string', value: '1111' },
|
||||
{ type: 'string', value: '0.1111' },
|
||||
{ type: 'string', value: 'TRUE' },
|
||||
{ type: 'string', value: 'FALSE' },
|
||||
];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('keep spaces in quoted strings', () => {
|
||||
const test = '"test space" 1111 0.1111 TRUE FALSE';
|
||||
const expected = [
|
||||
{ type: 'string', value: 'test space' },
|
||||
{ type: 'integer', value: 1111 },
|
||||
{ type: 'float', value: 0.1111 },
|
||||
{ type: 'T', value: true },
|
||||
{ type: 'F', value: false },
|
||||
];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('keep spaces escaped quotes', () => {
|
||||
const test = '"test \\" space" 1111 0.1111 TRUE FALSE';
|
||||
const expected = [
|
||||
{ type: 'string', value: 'test " space' },
|
||||
{ type: 'integer', value: 1111 },
|
||||
{ type: 'float', value: 0.1111 },
|
||||
{ type: 'T', value: true },
|
||||
{ type: 'F', value: false },
|
||||
];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('2 spaces', () => {
|
||||
const test = '1111 0.1111 TRUE FALSE';
|
||||
const expected = [
|
||||
{ type: 'integer', value: 1111 },
|
||||
{ type: 'float', value: 0.1111 },
|
||||
{ type: 'T', value: true },
|
||||
{ type: 'F', value: false },
|
||||
];
|
||||
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { Argument } from 'node-osc';
|
||||
import { splitWhitespace } from 'ontime-utils';
|
||||
|
||||
export function stringToOSCArgs(argsString: string | undefined): Argument[] {
|
||||
if (typeof argsString === 'undefined' || argsString === '') {
|
||||
return new Array<Argument>();
|
||||
}
|
||||
const matches = splitWhitespace(argsString);
|
||||
|
||||
if (!matches) {
|
||||
return new Array<Argument>();
|
||||
}
|
||||
|
||||
const parsedArguments: Argument[] = matches.map((argString: string) => {
|
||||
const argAsNum = Number(argString);
|
||||
// NOTE: number like: 1 2.0 33333
|
||||
if (!Number.isNaN(argAsNum)) {
|
||||
return { type: argString.includes('.') ? 'float' : 'integer', value: argAsNum };
|
||||
}
|
||||
|
||||
if (argString.startsWith('"') && argString.endsWith('"')) {
|
||||
// NOTE: "quoted string"
|
||||
return { type: 'string', value: argString.substring(1, argString.length - 1) };
|
||||
}
|
||||
|
||||
if (argString === 'TRUE') {
|
||||
// NOTE: Boolean true
|
||||
return { type: 'T', value: true };
|
||||
}
|
||||
|
||||
if (argString === 'FALSE') {
|
||||
// NOTE: Boolean false
|
||||
return { type: 'F', value: false };
|
||||
}
|
||||
|
||||
// NOTE: string
|
||||
return { type: 'string', value: argString };
|
||||
});
|
||||
|
||||
return parsedArguments;
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.11.0-beta.1",
|
||||
"version": "3.11.0",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"ontime",
|
||||
|
||||
@@ -7,15 +7,15 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
||||
clock: 0,
|
||||
timer: {
|
||||
addedTime: 0,
|
||||
current: null,
|
||||
duration: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null,
|
||||
finishedAt: null,
|
||||
phase: TimerPhase.None,
|
||||
playback: Playback.Stop,
|
||||
secondaryTimer: null,
|
||||
startedAt: null,
|
||||
current: null, // changes on every update
|
||||
duration: null, // only changes if event changes
|
||||
elapsed: null, // changes on every update
|
||||
expectedFinish: null, // change can only be initiated by user, can roll over midnight
|
||||
finishedAt: null, // can change on update or user action
|
||||
phase: TimerPhase.None, // can change on update or user action
|
||||
playback: Playback.Stop, // change initiated by user
|
||||
secondaryTimer: null, // change on every update
|
||||
startedAt: null, // change can only be initiated by user
|
||||
},
|
||||
onAir: false,
|
||||
message: {
|
||||
@@ -29,13 +29,13 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
||||
external: '',
|
||||
},
|
||||
runtime: {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
offset: 0,
|
||||
plannedStart: 0,
|
||||
plannedEnd: 0,
|
||||
actualStart: null,
|
||||
expectedEnd: null,
|
||||
selectedEventIndex: null, // changes if rundown changes or we load a new event
|
||||
numEvents: 0, // change initiated by user
|
||||
offset: 0, // changes at runtime
|
||||
plannedStart: 0, // only changes if event changes
|
||||
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
||||
actualStart: null, // set once we start the timer
|
||||
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
||||
},
|
||||
currentBlock: {
|
||||
block: null,
|
||||
|
||||
Generated
+148
-59
@@ -6,6 +6,9 @@ settings:
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@types/node':
|
||||
specifier: 20.17.16
|
||||
version: 20.17.16
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 7.16.1
|
||||
version: 7.16.1
|
||||
@@ -15,6 +18,9 @@ catalogs:
|
||||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3
|
||||
@@ -37,7 +43,7 @@ importers:
|
||||
version: 1.49.1
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 20.14.10
|
||||
version: 20.17.16
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 'catalog:'
|
||||
version: 7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.56.0)(typescript@5.5.3))(eslint@8.56.0)(typescript@5.5.3)
|
||||
@@ -188,7 +194,7 @@ importers:
|
||||
version: 7.16.1(eslint@8.56.0)(typescript@5.5.3)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))
|
||||
version: 4.2.1(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 8.56.0
|
||||
@@ -230,19 +236,19 @@ importers:
|
||||
version: 5.5.3
|
||||
vite:
|
||||
specifier: ^5.2.11
|
||||
version: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
version: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
vite-plugin-compression2:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3(rollup@4.17.2)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))
|
||||
version: 1.3.3(rollup@4.17.2)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))
|
||||
vite-plugin-svgr:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(rollup@4.17.2)(typescript@5.5.3)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))
|
||||
version: 4.2.0(rollup@4.17.2)(typescript@5.5.3)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(typescript@5.5.3)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))
|
||||
version: 4.3.1(typescript@5.5.3)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.6(@types/node@20.14.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
version: 2.1.6(@types/node@22.10.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
|
||||
apps/electron:
|
||||
devDependencies:
|
||||
@@ -310,8 +316,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
osc-min:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
specifier: 2.1.2
|
||||
version: 2.1.2
|
||||
sanitize-filename:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.3
|
||||
@@ -336,10 +342,7 @@ importers:
|
||||
version: 1.4.11
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 20.14.10
|
||||
'@types/node-osc':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
version: 20.17.16
|
||||
'@types/websocket':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
@@ -384,7 +387,7 @@ importers:
|
||||
version: 5.5.3
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.6(@types/node@20.14.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
version: 2.1.6(@types/node@20.17.16)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
|
||||
packages/types:
|
||||
devDependencies:
|
||||
@@ -439,7 +442,7 @@ importers:
|
||||
version: 5.5.3
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.6(@types/node@20.14.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
version: 2.1.6(@types/node@22.10.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -2055,12 +2058,15 @@ packages:
|
||||
'@types/multer@1.4.11':
|
||||
resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==}
|
||||
|
||||
'@types/node-osc@6.0.3':
|
||||
resolution: {integrity: sha512-f0JUTDVAlk/mV9RH6jE6g/8Y7l+Mvi3f1x7xv0RG3VI1PktaXEd60mR+4plpMS9VbRovFoPsS7qaaIDhFXdaEw==}
|
||||
|
||||
'@types/node@20.14.10':
|
||||
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
|
||||
|
||||
'@types/node@20.17.16':
|
||||
resolution: {integrity: sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==}
|
||||
|
||||
'@types/node@22.10.10':
|
||||
resolution: {integrity: sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==}
|
||||
|
||||
'@types/parse-json@4.0.0':
|
||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||
|
||||
@@ -3977,8 +3983,8 @@ packages:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
osc-min@2.1.1:
|
||||
resolution: {integrity: sha512-3Ai9vP8AAh1sCh5Zm8BUybfulv5OvqpTVa2BcUoTrLeD1Ss3cohIQpF1f18gx0CTbi9Dv4SvtdMb7y4g++7lYA==}
|
||||
osc-min@2.1.2:
|
||||
resolution: {integrity: sha512-dEhg7wBJ+QTHqY4iwnctkaFWzXLq+yHA2olxYfnxBw1fMy/iWJ/b7ovfXfwc/yxLXGiUHQhTxlQSHzL8ueHz6Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
p-cancelable@2.1.1:
|
||||
@@ -4756,6 +4762,12 @@ packages:
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
@@ -6717,13 +6729,13 @@ snapshots:
|
||||
'@types/body-parser@1.19.2':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.35
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/cacheable-request@6.0.3':
|
||||
dependencies:
|
||||
'@types/http-cache-semantics': 4.0.4
|
||||
'@types/keyv': 3.1.4
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
'@types/responselike': 1.0.3
|
||||
|
||||
'@types/color-convert@2.0.0':
|
||||
@@ -6738,11 +6750,11 @@ snapshots:
|
||||
|
||||
'@types/connect@3.4.35':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/cors@2.8.17':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@@ -6752,7 +6764,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@4.17.33':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
|
||||
@@ -6765,7 +6777,7 @@ snapshots:
|
||||
|
||||
'@types/fs-extra@9.0.13':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
|
||||
'@types/http-cache-semantics@4.0.4': {}
|
||||
|
||||
@@ -6773,7 +6785,7 @@ snapshots:
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
|
||||
'@types/lodash.mergewith@4.6.7':
|
||||
dependencies:
|
||||
@@ -6789,19 +6801,23 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/express': 4.17.17
|
||||
|
||||
'@types/node-osc@6.0.3':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
|
||||
'@types/node@20.14.10':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@20.17.16':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/node@22.10.10':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
||||
'@types/parse-json@4.0.0': {}
|
||||
|
||||
'@types/plist@3.0.5':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
xmlbuilder: 15.1.1
|
||||
optional: true
|
||||
|
||||
@@ -6823,7 +6839,7 @@ snapshots:
|
||||
|
||||
'@types/responselike@1.0.3':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
|
||||
'@types/scheduler@0.16.2': {}
|
||||
|
||||
@@ -6832,22 +6848,22 @@ snapshots:
|
||||
'@types/serve-static@1.15.0':
|
||||
dependencies:
|
||||
'@types/mime': 3.0.1
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/verror@1.10.9':
|
||||
optional: true
|
||||
|
||||
'@types/websocket@1.0.5':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/ws@8.5.10':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 22.10.10
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@7.16.1(@typescript-eslint/parser@7.16.1(eslint@8.56.0)(typescript@5.5.3))(eslint@8.56.0)(typescript@5.5.3)':
|
||||
@@ -6974,14 +6990,14 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@vitejs/plugin-react@4.2.1(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))':
|
||||
'@vitejs/plugin-react@4.2.1(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.0
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6992,13 +7008,21 @@ snapshots:
|
||||
chai: 5.1.2
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.6(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))':
|
||||
'@vitest/mocker@2.1.6(vite@5.2.11(@types/node@20.17.16)(sass@1.57.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.6
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.14
|
||||
optionalDependencies:
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@20.17.16)(sass@1.57.1)
|
||||
|
||||
'@vitest/mocker@2.1.6(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.6
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.14
|
||||
optionalDependencies:
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
|
||||
'@vitest/pretty-format@2.1.6':
|
||||
dependencies:
|
||||
@@ -9106,7 +9130,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
osc-min@2.1.1: {}
|
||||
osc-min@2.1.2: {}
|
||||
|
||||
p-cancelable@2.1.1: {}
|
||||
|
||||
@@ -9896,6 +9920,10 @@ snapshots:
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
universalify@0.2.0:
|
||||
@@ -9968,13 +9996,13 @@ snapshots:
|
||||
extsprintf: 1.4.1
|
||||
optional: true
|
||||
|
||||
vite-node@2.1.6(@types/node@20.14.10)(sass@1.57.1):
|
||||
vite-node@2.1.6(@types/node@20.17.16)(sass@1.57.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7
|
||||
es-module-lexer: 1.5.4
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@20.17.16)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -9985,50 +10013,77 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-plugin-compression2@1.3.3(rollup@4.17.2)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1)):
|
||||
vite-node@2.1.6(@types/node@22.10.10)(sass@1.57.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7
|
||||
es-module-lexer: 1.5.4
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-plugin-compression2@1.3.3(rollup@4.17.2)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.17.2)
|
||||
tar-mini: 0.2.0
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
vite-plugin-svgr@4.2.0(rollup@4.17.2)(typescript@5.5.3)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1)):
|
||||
vite-plugin-svgr@4.2.0(rollup@4.17.2)(typescript@5.5.3)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.17.2)
|
||||
'@svgr/core': 8.1.0(typescript@5.5.3)
|
||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.5.3))
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite-tsconfig-paths@4.3.1(typescript@5.5.3)(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1)):
|
||||
vite-tsconfig-paths@4.3.1(typescript@5.5.3)(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1)):
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.0.2(typescript@5.5.3)
|
||||
optionalDependencies:
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite@5.2.11(@types/node@20.14.10)(sass@1.57.1):
|
||||
vite@5.2.11(@types/node@20.17.16)(sass@1.57.1):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.38
|
||||
rollup: 4.17.2
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
fsevents: 2.3.3
|
||||
sass: 1.57.1
|
||||
|
||||
vitest@2.1.6(@types/node@20.14.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1):
|
||||
vite@5.2.11(@types/node@22.10.10)(sass@1.57.1):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.38
|
||||
rollup: 4.17.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.10.10
|
||||
fsevents: 2.3.3
|
||||
sass: 1.57.1
|
||||
|
||||
vitest@2.1.6(@types/node@20.17.16)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.6
|
||||
'@vitest/mocker': 2.1.6(vite@5.2.11(@types/node@20.14.10)(sass@1.57.1))
|
||||
'@vitest/mocker': 2.1.6(vite@5.2.11(@types/node@20.17.16)(sass@1.57.1))
|
||||
'@vitest/pretty-format': 2.1.6
|
||||
'@vitest/runner': 2.1.6
|
||||
'@vitest/snapshot': 2.1.6
|
||||
@@ -10044,11 +10099,47 @@ snapshots:
|
||||
tinyexec: 0.3.1
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.11(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite-node: 2.1.6(@types/node@20.14.10)(sass@1.57.1)
|
||||
vite: 5.2.11(@types/node@20.17.16)(sass@1.57.1)
|
||||
vite-node: 2.1.6(@types/node@20.17.16)(sass@1.57.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
'@types/node': 20.17.16
|
||||
happy-dom: 16.7.2
|
||||
jsdom: 21.1.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@2.1.6(@types/node@22.10.10)(happy-dom@16.7.2)(jsdom@21.1.0)(sass@1.57.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.6
|
||||
'@vitest/mocker': 2.1.6(vite@5.2.11(@types/node@22.10.10)(sass@1.57.1))
|
||||
'@vitest/pretty-format': 2.1.6
|
||||
'@vitest/runner': 2.1.6
|
||||
'@vitest/snapshot': 2.1.6
|
||||
'@vitest/spy': 2.1.6
|
||||
'@vitest/utils': 2.1.6
|
||||
chai: 5.1.2
|
||||
debug: 4.3.7
|
||||
expect-type: 1.1.0
|
||||
magic-string: 0.30.14
|
||||
pathe: 1.1.2
|
||||
std-env: 3.8.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.1
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.11(@types/node@22.10.10)(sass@1.57.1)
|
||||
vite-node: 2.1.6(@types/node@22.10.10)(sass@1.57.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.10.10
|
||||
happy-dom: 16.7.2
|
||||
jsdom: 21.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -10145,8 +10236,6 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.13.0: {}
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
xlsx@0.18.5:
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ catalog:
|
||||
typescript: 5.5.3
|
||||
"@typescript-eslint/eslint-plugin": 7.16.1
|
||||
"@typescript-eslint/parser": 7.16.1
|
||||
"@types/node": 20.14.10
|
||||
"@types/node": 20.17.16
|
||||
prettier: 3.3.1
|
||||
eslint: 8.56.0
|
||||
eslint-config-prettier: 9.1.0
|
||||
|
||||
Reference in New Issue
Block a user