Feat/navigate (#81)

* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
This commit is contained in:
Carlos Valente
2022-01-04 22:12:59 +01:00
committed by GitHub
parent dd43d5e20a
commit 2fa397548a
74 changed files with 2592 additions and 1428 deletions
+64
View File
@@ -0,0 +1,64 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: ontime_test_CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
# Utils server
- name: Utils - Install dependencies
run: yarn install
working-directory: ./server/utils
- name: Utils - run link command
run: yarn link
working-directory: ./server/utils
# React
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./client
- name: React - Install dependencies
run: yarn install
working-directory: ./client
- name: React - Run tests
run: yarn test:pipeline
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./server/src
- name: Server - Install dependencies
run: yarn install
working-directory: ./server/src
# App
- name: Electron - Install dependencies
run: yarn install
working-directory: ./server
- name: Electron - Run tests
run: yarn test
working-directory: ./server
+7
View File
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true
}
+11 -2
View File
@@ -21,8 +21,11 @@ From here, any device in the same network with a browser is able to render the v
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden). You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
In case of unattended machines or automations, it is possible to use different URL to recall individual views In case of unattended machines or automations, it is possible to use different URL to recall individual views
and extend with using the URL aliases feature
``` ```
For the presentation views...
-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/preseter > Presenter / Stage timer view IP.ADDRESS:4001/preseter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view IP.ADDRESS:4001/sm > Stage Manager / Backstage view
@@ -30,6 +33,11 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock IP.ADDRESS:4001/studio > Studio Clock
...and for the editor (the control interface, same as the app)
-------------------------------------------------------------
IP.ADDRESS:4001/studio > Studio Clock
``` ```
More documentation available [here](https://cpvalente.gitbook.io/ontime/) More documentation available [here](https://cpvalente.gitbook.io/ontime/)
@@ -47,6 +55,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- [x] Open Sound Control (OSC) Control and Feedback - [x] Open Sound Control (OSC) Control and Feedback
- [x] Roll mode: run independently using the system clock - [x] Roll mode: run independently using the system clock
- [x] Import event list from Excel - [x] Import event list from Excel
- [x] URL Aliases (define configurable aliases to ease onsite setup)
- [x] Logging view
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app)
## Unopinionated ## Unopinionated
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work. We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
@@ -77,8 +88,6 @@ These will be implemented in a development friendly order unless there is user d
- [ ] Companion module - [ ] Companion module
- [ ] Lower Third Manager - [ ] Lower Third Manager
- [ ] Note only event - [ ] Note only event
- [ ] URL Aliases (define configurable aliases to ease onsite setup)
- [ ] Logging view
- [ ] Reach Schedule: way to speedup timer to meet a deadline - [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] vMix integration - [ ] vMix integration
+8 -7
View File
@@ -3,26 +3,26 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "^1.7.1", "@chakra-ui/react": "^1.7.3",
"@emotion/react": "^11.5.0", "@emotion/react": "^11.7.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0", "axios": "^0.24.0",
"framer-motion": "^4.1.6", "framer-motion": "^4.1.6",
"jotai": "^0.16.5", "jotai": "^0.16.5",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2", "react": "17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-qr-code": "^2.0.2", "react-qr-code": "2.0.3",
"react-query": "^3.34.5", "react-query": "^3.34.5",
"react-router": "^5.2.0", "react-router-dom": "^6.2.1",
"react-router-dom": "^5.2.0",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"socket.io-client": "^4.3.2", "socket.io-client": "4.4.0",
"typeface-open-sans": "^1.1.13", "typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0", "use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
@@ -31,6 +31,7 @@
"start": "set BROWSER=none&&react-scripts start", "start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"browserslist": { "browserslist": {
+42 -34
View File
@@ -1,18 +1,17 @@
import { lazy, Suspense, useCallback, useEffect } from 'react'; import { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss'; import './App.scss';
import { QueryClient, QueryClientProvider } from 'react-query';
import SocketProvider from 'app/context/socketContext';
import withSocket from 'features/viewers/ViewWrapper'; import withSocket from 'features/viewers/ViewWrapper';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import ProtectRoute from './common/components/protectRoute/ProtectRoute';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/Editor')); const Editor = lazy(() => import('features/editors/Editor'));
const PresenterView = lazy(() => const PresenterView = lazy(() =>
import('features/viewers/presenter/PresenterView') import('features/viewers/presenter/PresenterView')
); );
const PresenterSimple = lazy(() =>
import('features/viewers/presenter/PresenterSimple')
);
const StageManager = lazy(() => const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager') import('features/viewers/backstage/StageManager')
); );
@@ -21,18 +20,9 @@ const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper') import('features/viewers/production/lower/LowerWrapper')
); );
const Pip = lazy(() => import('features/viewers/production/Pip')); const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock')); const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const queryClient = new QueryClient();
// Seemed to cause issues
// broadcastQueryClient({
// queryClient,
// broadcastChannel: 'ontime',
// });
const SPresenter = withSocket(PresenterView); const SPresenter = withSocket(PresenterView);
const SPresenterSimple = withSocket(PresenterSimple);
const SStageManager = withSocket(StageManager); const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public); const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower); const SLowerThird = withSocket(Lower);
@@ -40,6 +30,10 @@ const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock); const SStudio = withSocket(StudioClock);
function App() { function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => { const handleKeyPress = useCallback((e) => {
// check if the alt key is pressed // check if the alt key is pressed
@@ -65,33 +59,47 @@ function App() {
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
// navigate if is alias route
useEffect(() => {
if (data == null) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return ( return (
<SocketProvider>
<QueryClientProvider client={queryClient}>
<div className='App'> <div className='App'>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={null}> <Suspense fallback={null}>
<Switch> <Routes>
<Route exact path='/' component={SPresenter} /> <Route path='/' element={<SPresenter />} />
<Route exact path='/sm' component={SStageManager} /> <Route path='/sm' element={<SStageManager />} />
<Route exact path='/speaker' component={SPresenter} /> <Route path='/speaker' element={<SPresenter />} />
<Route exact path='/presenter' component={SPresenter} /> <Route path='/presenter' element={<SPresenter />} />
<Route exact path='/stage' component={SPresenter} /> <Route path='/stage' element={<SPresenter />} />
<Route exact path='/presentersimple' component={SPresenterSimple} /> <Route path='/public' element={<SPublic />} />
<Route exact path='/editor' component={Editor} /> <Route path='/pip' element={<SPip />} />
<Route exact path='/public' component={SPublic} /> <Route path='/studio' element={<SStudio />} />
<Route exact path='/pip' component={SPip} /> {/*/!* Lower cannot have fallback *!/*/}
<Route exact path='/studio' component={SStudio} /> <Route path='/lower' element={<SLowerThird />} />
{/* Lower cannot have fallback */} {/*/!* Protected Routes *!/*/}
<Route exact path='/lower' component={SLowerThird} /> <Route
path='/editor'
element={
<ProtectRoute>
<Editor />
</ProtectRoute>
}
/>
{/* Send to default if nothing found */} {/* Send to default if nothing found */}
<Route component={SPresenter} /> <Route path='*' element={<SPresenter />} />
</Switch> </Routes>
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</QueryClientProvider>
</SocketProvider>
); );
} }
-15
View File
@@ -1,15 +0,0 @@
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
+2
View File
@@ -1,8 +1,10 @@
export const NODE_PORT = 4001; export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event'; export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const EVENTS_TABLE = 'events'; export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo'; export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings'; export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
const calculateServer = () => { const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`); return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
+30
View File
@@ -9,6 +9,18 @@ export const ontimePlaceholderInfo = {
}, },
}; };
export const ontimePlaceholderSettings = {
pinCode: null,
};
export const eventPlaceholderSettings = {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
};
export const oscPlaceholderSettings = { export const oscPlaceholderSettings = {
port: '', port: '',
portOut: '', portOut: '',
@@ -74,6 +86,15 @@ export const ontimeVars = [
}, },
]; ];
export const getSettings = async () => {
const res = await axios.get(`${ontimeURL}/settings`);
return res.data;
};
export const postSettings = async (data) => {
return await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => { export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`); const res = await axios.get(`${ontimeURL}/info`);
return res.data; return res.data;
@@ -83,6 +104,15 @@ export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data); return await axios.post(`${ontimeURL}/info`, data);
}; };
export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`);
return res.data;
};
export const postAliases = async (data) => {
return await axios.post(`${ontimeURL}/aliases`, data);
};
export const getOSC = async () => { export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`); const res = await axios.get(`${ontimeURL}/osc`);
return res.data; return res.data;
+14
View File
@@ -0,0 +1,14 @@
// Exported viewer links
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
export const viewerLinks = [
{ link: speakerLink, label: 'Speaker Screen' },
{ link: smLink, label: 'Backstage Screen' },
{ link: publicLink, label: 'Public Screen' },
{ link: pipLink, label: 'Picture in Picture' },
{ link: studioLink, label: 'Studio Clock' }
];
+37
View File
@@ -0,0 +1,37 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
export const AppContext = createContext({
auth: false,
data: {
pinCode: null
}
});
export const AppContextProvider = (props) => {
const [auth, setAuth] = useState(true);
const { data } = useFetch(APP_SETTINGS, getSettings);
useEffect(() => {
if (data == null) return;
if (data?.pinCode === null || data?.pinCode === '') {
setAuth(true);
} else {
setAuth(false);
}
},[data])
const validate = useCallback((pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
}, [data]);
return (
<AppContext.Provider value={{ auth, validate }}>
{props.children}
</AppContext.Provider>
);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSocket } from './socketContext'; import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react'; import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-server/utils/generate_id'; import { generateId } from 'ontime-utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-server/utils/time'; import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
export const LoggingContext = createContext({ export const LoggingContext = createContext({
logData: [], logData: [],
-7
View File
@@ -1,7 +0,0 @@
export const userConfig = {
timerColorOnPause: '#555',
timerColorOnRunning: '#FFF',
timerColorOnMessage: '#CCC',
timerColorOnTimeOver: '#F00',
overTimeText: '',
}
-2
View File
@@ -1,2 +0,0 @@
export const clamp = (num, a, b) =>
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
@@ -0,0 +1,27 @@
import { validateAlias } from '../aliases';
describe('An alias 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) => (
test(`${t}`, () => {
expect(validateAlias(t).status).toBeFalsy();
})
)
);
});
@@ -0,0 +1,21 @@
import { clamp } from '../math';
test('Clamps a set of numbers correctly', () => {
const testCases = [
{ num: 10, min: 0, max: 20, result: 10 },
{ num: 0, min: 0, max: 20, result: 0 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: -20, min: 0, max: 20, result: 0 },
{ num: -0, min: 0, max: 20, result: 0 },
{ num: -50, min: -30, max: -20, result: -30 },
{ num: -50, min: 0, max: 0, result: 0 },
{ num: 50.5, min: 0, max: 100, result: 50.5 },
{ num: 50, min: 0, max: 20.32, result: 20.32 },
{ num: 10, min: 20.32, max: 40, result: 20.32 }
];
testCases.forEach((t) => (
expect(clamp(t.num, t.min, t.max)).toBe(t.result)
));
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Validates an alias against defined parameters
* @param {string} alias
* @returns {{message: string, status: boolean}}
*/
export const validateAlias = (alias) => {
const valid = { status: true, message: 'ok' };
if (alias === '' || alias == null) {
// cannot be empty
valid.status = false;
valid.message = 'should not be empty';
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
// cannot contain http, https or www
valid.status = false;
valid.message = 'should not include http, https, www';
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
// aliases cannot contain hostname
valid.status = false;
valid.message = 'should not include hostname';
} else if (alias.includes('editor')) {
// no editor
valid.status = false;
valid.message = 'No aliases to editor page allowed';
}
return valid;
};
+9
View File
@@ -0,0 +1,9 @@
/**
* Clamps a value between a min and a max
* @param {number} num - Value to clamp
* @param {number} min - min value
* @param {number} max - max value
* @returns {number}
*/
export const clamp = (num, min, max) =>
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
@@ -1,5 +1,5 @@
import EditableTimer from 'common/input/EditableTimer'; import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react'; import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext'; import { LoggingContext } from '../../../app/context/LoggingContext';
@@ -1,4 +1,4 @@
import { clamp } from 'app/utils'; import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css'; import styles from './MyProgressBar.module.css';
export default function MyProgressBar(props) { export default function MyProgressBar(props) {
+1 -7
View File
@@ -1,5 +1,5 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Link, Redirect } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
@@ -57,7 +57,6 @@ export default function NavLogo(props) {
to='/presenter' to='/presenter'
className={style.navItem} className={style.navItem}
tabIndex={1} tabIndex={1}
onKeyDownCapture={() => <Redirect push to='/presenter' />}
> >
Presenter Presenter
</Link> </Link>
@@ -65,7 +64,6 @@ export default function NavLogo(props) {
to='/sm' to='/sm'
className={style.navItem} className={style.navItem}
tabIndex={2} tabIndex={2}
onKeyDownCapture={() => <Redirect push to='/sm' />}
> >
Backstage Backstage
</Link> </Link>
@@ -73,7 +71,6 @@ export default function NavLogo(props) {
to='/public' to='/public'
className={style.navItem} className={style.navItem}
tabIndex={3} tabIndex={3}
onKeyDownCapture={() => <Redirect push to='/public' />}
> >
Public Public
</Link> </Link>
@@ -81,7 +78,6 @@ export default function NavLogo(props) {
to='/lower' to='/lower'
className={style.navItem} className={style.navItem}
tabIndex={4} tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/lower' />}
> >
Lower Thirds Lower Thirds
</Link> </Link>
@@ -89,7 +85,6 @@ export default function NavLogo(props) {
to='/pip' to='/pip'
className={style.navItem} className={style.navItem}
tabIndex={4} tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/pip' />}
> >
PIP PIP
</Link> </Link>
@@ -97,7 +92,6 @@ export default function NavLogo(props) {
to='/studio' to='/studio'
className={style.navItem} className={style.navItem}
tabIndex={5} tabIndex={5}
onKeyDownCapture={() => <Redirect push to='/studio' />}
> >
Studio Clock Studio Clock
</Link> </Link>
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button';
import { FiCheck } from 'react-icons/fi';
import { AppContext } from '../../../app/context/AppContext';
import { useContext, useEffect, useState } from 'react';
export default function ProtectRoute(props) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
// Set window title
useEffect(() => {
document.title = 'ontime';
}, []);
const handleValidation = () => {
const r = validate(pin);
if (!r) {
setFailed(true);
}
}
return (
<>
{!isLocal && !auth ? (
<div className={style.container}>
ontime
<div className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
aria-label='Enter'
size='lg'
isRound
icon={<FiCheck />}
style={{ fontSize: '1.5em' }}
onClick={() => handleValidation()}
/>
</div>
</div>
) : (
props.children
)}
</>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired
};
@@ -0,0 +1,47 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
.container {
background: #222;
display: grid;
place-content: center;
height: 100vh;
padding-bottom: 30vh;
color: $ontime-pink;
font-family: 'Open Sans', sans-serif;
font-weight: 200;
text-align: center;
font-size: 3vw;
}
.pin,
.pin__failed {
display: flex;
gap: 10px;
padding: 20px;
input {
border-radius: 50%;
}
button {
margin-left: 20px;
}
}
.pin__failed {
input {
animation: colourFade 1.5s ease;
}
}
@keyframes colourFade {
from {
background: $ontime-pink;
}
to {
background: rgba($ontime-pink, 0);
}
}
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css'; import style from './Paginator.module.css';
export default function TodayItem(props) { export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props; const { selected, timeStart, timeEnd, title, backstageEvent } = props;
+1 -1
View File
@@ -4,7 +4,7 @@ import {
isTimeString, isTimeString,
timeStringToMillis, timeStringToMillis,
} from '../utils/dateConfig'; } from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css'; import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
+1 -1
View File
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
/** /**
* @description From a list of events, returns only events of type event with calculated delays * @description From a list of events, returns only events of type event with calculated delays
+13
View File
@@ -0,0 +1,13 @@
/**
* Handles link to external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export default function handleLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
@@ -1,6 +1,6 @@
import style from './PlaybackControl.module.scss'; import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown'; import Countdown from 'common/components/countdown/Countdown';
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
import {Tooltip} from '@chakra-ui/react'; import {Tooltip} from '@chakra-ui/react';
import {Button} from '@chakra-ui/button'; import {Button} from '@chakra-ui/button';
import {memo} from 'react'; import {memo} from 'react';
+3 -1
View File
@@ -24,12 +24,14 @@ export default function Editor() {
return ( return (
<LoggingProvider> <LoggingProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} /> <ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}> <div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}> <Box id='settings' className={styles.settings}>
<ErrorBoundary> <ErrorBoundary>
<MenuBar onOpen={onOpen} /> <MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary> </ErrorBoundary>
</Box> </Box>
+2 -1
View File
@@ -1,4 +1,5 @@
@use '../../main' as *; @use '../../styles/main' as *;
@use '../../styles/mixins' as *;
@mixin container { @mixin container {
margin-top: 1em; margin-top: 1em;
@@ -1,5 +1,6 @@
@use 'Info.module' as *; @use 'Info.module' as *;
@use '../../main' as *; @use '../../styles/main' as *;
@use '../../styles/mixins' as *;
.container, .container,
.container__expanded{ .container__expanded{
+1 -8
View File
@@ -4,6 +4,7 @@ import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss'; import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar'; import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import handleLink from '../../common/utils/handleLink';
export default function InfoNif() { export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, { const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -12,14 +13,6 @@ export default function InfoNif() {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001'; const baseURL = 'http://__IP__:4001';
const handleLink = (url) => {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
};
return ( return (
<div className={style.container}> <div className={style.container}>
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/> <CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
+21 -8
View File
@@ -6,14 +6,15 @@ import SettingsIconBtn from './buttons/SettingsIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn'; import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn'; import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn'; import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.css'; import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn'; import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn'; import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react'; import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
export default function MenuBar(props) { export default function MenuBar(props) {
const { onOpen } = props; const { isOpen, onOpen } = props;
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null); const hiddenFileInput = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -33,6 +34,10 @@ export default function MenuBar(props) {
} }
}; };
const buttonStyle = {
fontSize: '1.5em'
};
const handleUpload = (event) => { const handleUpload = (event) => {
const fileUploaded = event.target.files[0]; const fileUploaded = event.target.files[0];
if (fileUploaded == null) return; if (fileUploaded == null) return;
@@ -93,25 +98,27 @@ export default function MenuBar(props) {
<> <>
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} /> <QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
<MaxIconBtn <MaxIconBtn
style={{ fontSize: '1.5em' }} style={{ ...buttonStyle }}
size='lg' size='lg'
clickhandler={() => handleIPC('max')} clickhandler={() => handleIPC('max')}
/> />
<MinIconBtn <MinIconBtn
style={{ fontSize: '1.5em' }} style={{ ...buttonStyle }}
size='lg' size='lg'
clickhandler={() => handleIPC('min')} clickhandler={() => handleIPC('min')}
/> />
<div className={style.gap} /> <div className={style.gap} />
<HelpIconBtn <HelpIconBtn
style={{ fontSize: '1.5em' }} style={{ ...buttonStyle }}
size='lg' size='lg'
clickhandler={() => handleIPC('help')} clickhandler={() => handleIPC('help')}
/> />
<SettingsIconBtn <SettingsIconBtn
style={{ fontSize: '1.5em' }} style={{...buttonStyle}}
size='lg' size='lg'
className={isOpen ? style.open : ''}
clickhandler={onOpen} clickhandler={onOpen}
isRound
/> />
<div className={style.gap} /> <div className={style.gap} />
<input <input
@@ -122,15 +129,21 @@ export default function MenuBar(props) {
accept='.json, .xlsx' accept='.json, .xlsx'
/> />
<UploadIconBtn <UploadIconBtn
style={{ fontSize: '1.5em' }} style={{ ...buttonStyle }}
size='lg' size='lg'
clickhandler={handleClick} clickhandler={handleClick}
/> />
<DownloadIconBtn <DownloadIconBtn
style={{ fontSize: '1.5em' }} style={{ ...buttonStyle }}
size='lg' size='lg'
clickhandler={handleDownload} clickhandler={handleDownload}
/> />
</> </>
); );
} }
MenuBar.propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
};
@@ -1,3 +0,0 @@
.gap {
height: 1em;
}
@@ -0,0 +1,9 @@
@use '../../styles/main' as *;
.gap {
height: 1em;
}
.open {
background: $light-bg;
}
+278 -131
View File
@@ -1,174 +1,321 @@
import { Button, IconButton } from '@chakra-ui/button'; import { Button, IconButton } from '@chakra-ui/button';
import { FiPlus, FiMinus } from 'react-icons/fi'; import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { fetchEvent } from 'app/api/eventApi'; import { getAliases, postAliases } from '../../app/api/ontimeApi';
import { useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants'; import { ALIASES } from 'app/api/apiConstants';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { viewerLinks } from '../../app/appConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import { validateAlias } from '../../app/utils/aliases';
import { Tooltip } from '@chakra-ui/tooltip';
import SubmitContainer from './SubmitContainer';
import handleLink from '../../common/utils/handleLink';
export default function AliasesModal() { export default function AliasesModal() {
const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent); const { data, status, refetch } = useFetch(ALIASES, getAliases);
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [aliases, setAliases] = useState([]);
const host = window.location.host;
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setAliases([...data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => { const submitHandler = async (event) => {
event.preventDefault(); event.preventDefault();
// NOTHING HERE YET setSubmitting(true);
const validatedAliases = [...aliases];
let errors = false;
for (const alias of validatedAliases) {
// validate url
const isURLValid = validateAlias(alias.pathAndParams);
if (!isURLValid.status) {
alias.urlError = isURLValid.message;
errors = true;
} else {
alias.urlError = undefined;
}
// validate alias
const isAliasValid = validateAlias(alias.alias);
if (!isAliasValid.status) {
alias.aliasError = isAliasValid.message;
errors = true;
} else {
alias.aliasError = undefined;
}
}
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
}
setSubmitting(false);
}; };
// Hardcoded links for now /**
// it will need dynamic PORT assignment * Creates a new alias in state with a temporary id
const speakerLink = 'http://localhost:4001/speaker'; */
const smLink = 'http://localhost:4001/sm'; const addNew = () => {
const publicLink = 'http://localhost:4001/public'; if (aliases.length > 20) {
const pipLink = 'http://localhost:4001/pip'; emitError('Maximum amount of aliases reacted (20)');
const studioLink = 'http://localhost:4001/studio'; return;
}
const emptyAlias = {
id: Math.floor(Math.random() * 1000),
enabled: false,
alias: '',
pathAndParams: '',
};
setAliases((prevState) => [...prevState, emptyAlias]);
setChanged(true);
};
/**
* Deletes an alias by a given id
* @param {string} id - id of alias to delete
*/
const deleteAlias = (id) => {
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
setChanged(true);
};
/**
* Sets enabled flag to true / false
* @param {string} id - object id
* @param {boolean} isEnabled - whether to enable / disable flag
*/
const setEnabled = (id, isEnabled) => {
const aliasesState = [...aliases];
for (const a of aliasesState) {
if (a.id === id) {
if (isEnabled) {
if (a.alias === '' || a.pathAndParams === '') {
emitError('Alias incomplete');
break;
}
const isRepeated = aliases.some(
(r) => a.alias === r.alias && r.enabled
);
if (isRepeated) {
emitError('There is already an alias with this name');
break;
}
}
a.enabled = isEnabled;
break;
}
}
setChanged(true);
setAliases(aliasesState);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {number} index - index of item in array
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (index, field, value) => {
const temp = [...aliases];
temp[index][field] = value;
setAliases(temp);
setChanged(true);
};
return ( return (
<>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}> <ModalBody className={style.modalBody}>
<p className={style.notes}> <p className={style.notes}>
Configure easy to use URL Aliases Configure easy to use URL Aliases
<br /> <br />
Feature is not yet implemented 🔥 Changes take effect on save 🔥
</p> </p>
<form onSubmit={submitHandler}>
<span>Default URLs</span> <div className={style.modalFields}>
<div className={style.hSeparator}>Default URLs</div>
<div className={style.highNotes}> <div className={style.blockNotes}>
<p className={style.flexNote}> {viewerLinks.map((l) => (
Presenter Screen <br />
<a <a
href={speakerLink} href={l.link}
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
className={style.label} className={style.flexNote}
key={l.link}
onClick={() => handleLink(`${host}/${l.link}`)}
> >
{speakerLink} {`${l.label} - ${l.link}`}
</a> </a>
</p> ))}
<p className={style.flexNote}>
Backstage / Stage Manager Screen <br />
<a
href={smLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{smLink}
</a>
</p>
<p className={style.flexNote}>
Public / Foyer Screen <br />
<a
href={publicLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{publicLink}
</a>
</p>
<p className={style.flexNote}>
Picture in Picture Screen <br />
<a
href={pipLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{pipLink}
</a>
</p>
<p className={style.flexNote}>
Studio Clock<br />
<a
href={studioLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{studioLink}
</a>
</p>
</div> </div>
<div className={style.hSeparator}>Custom Aliases</div>
<span>Manage custom aliases</span> <div className={style.blockNotes}>
<div className={style.modalInline}> <span className={style.inlineFlex}>
<Input <FiInfo color='#2b6cb0' fontSize={'2em'} />
size='sm' URL aliases are useful in two main scenarios
name='URL' </span>
placeholder='A long URL' <span className={style.labelNote}>Complicated URLs</span>
autoComplete='off' <br />
value={'A long URL'} eg. a lower third url with some custom parameters
onChange={(event) => { <table>
// Nothing here yet <tbody>
}} <tr>
isDisabled={true} <td className={style.labelNote} style={{ width: '30%' }}>
/> Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
</tbody>
</table>
<br />
<span className={style.labelNote}>
URLs to be changed dynamically
</span>
<br />
eg. an unattended screen that you would need to change route from
the app
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
</tbody>
</table>
</div>
<div
className={style.inlineAliasPlaceholder}
style={{ padding: '0.5em 0' }}
>
<span className={style.labelNote}>Alias</span>
<span className={style.labelNote}>Page URL</span>
</div>
{aliases.map((alias, index) => (
<div key={alias.id}>
<div className={style.inlineAlias}>
<Input <Input
size='sm' size='sm'
variant='flushed'
name='Alias' name='Alias'
placeholder='A nice alias' placeholder='URL Alias'
autoComplete='off' autoComplete='off'
value={'A nice alias'} value={alias.alias}
onChange={(event) => { isInvalid={alias.aliasError}
// Nothing here yet onChange={(event) =>
}} handleChange(index, 'alias', event.target.value)
isDisabled={true} }
/> />
<IconButton <Input
size='sm' size='sm'
fontSize={'0.75em'}
variant='flushed'
name='URL'
placeholder='URL (portion after ontime Port)'
autoComplete='off'
value={alias.pathAndParams}
isInvalid={alias.urlError}
onChange={(event) =>
handleChange(index, 'pathAndParams', event.target.value)
}
/>
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={500}>
<a
href='#!'
target='_blank'
rel='noreferrer'
onClick={(e) => {
e.preventDefault();
handleLink(`http://${host}/${alias.pathAndParams}`);
}}
/>
</Tooltip>
<Tooltip label='Enable alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiSun />}
colorScheme='blue'
variant={alias.enabled ? null : 'outline'}
onClick={() => setEnabled(alias.id, !alias.enabled)}
/>
</Tooltip>
<Tooltip label='Delete alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiMinus />} icon={<FiMinus />}
colorScheme='red' colorScheme='red'
disabled onClick={() => deleteAlias(alias.id)}
/> />
</Tooltip>
</div> </div>
<div className={style.separator} /> {alias.aliasError ? (
<div className={style.modalInline}> <div
<Input className={style.error}
size='sm' >{`Alias error: ${alias.aliasError}`}</div>
name='URL' ) : null}
placeholder='URL' {alias.urlError ? (
autoComplete='off' <div
value={'URL'} className={style.error}
onChange={(event) => { >{`URL error: ${alias.urlError}`}</div>
// Nothing here yet ) : null}
}}
isDisabled={true}
/>
<Input
size='sm'
name='Alias'
placeholder='Alias'
autoComplete='off'
value={'Alias'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<IconButton
size='sm'
icon={<FiPlus />}
colorScheme='blue'
disabled
/>
</div> </div>
<div className={style.submitContainer}> ))}
<Button
colorScheme='blue' <div
type='submit' className={style.inlineAliasPlaceholder}
isLoading={submitting} style={{ padding: '0.5em 0' }}
disabled={true}
> >
Save <Button
size='xs'
colorScheme='blue'
variant='outline'
onClick={() => addNew()}
>
Add new
</Button> </Button>
</div> </div>
</ModalBody> </div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form> </form>
</> </ModalBody>
); );
} }
+114 -127
View File
@@ -1,183 +1,170 @@
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react'; import {
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi'; FormControl,
FormLabel,
Input,
PinInput,
PinInputField,
} from '@chakra-ui/react';
import {
getSettings,
ontimePlaceholderSettings,
postSettings,
} from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants'; import { APP_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import { IconButton } from '@chakra-ui/button';
import { FiEye } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function AppSettingsModal() { export default function AppSettingsModal() {
const { data, status } = useFetch(OSC_SETTINGS, getOSC); const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
const { emitError } = useContext(LoggingContext); const { emitError, emitWarning } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings); const [formData, setFormData] = useState(ontimePlaceholderSettings);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true);
/**
* Set formdata from server state
*/
useEffect(() => { useEffect(() => {
if (data == null) return; if (data == null) return;
setFormData({ ...data }); if (changed) return;
}, [data]); setFormData({
pinCode: data.pinCode,
});
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => { const submitHandler = async (event) => {
event.preventDefault(); event.preventDefault();
setSubmitting(true);
const f = formData; const f = formData;
let e = { status: false, message: '' }; let e = { status: false, message: '' };
// Validate fields // Validate fields
if (f.port < 1024 || f.port > 65535) { if (f.pinCode === '' || f.pinCode == null) {
// Port in incorrect range
e.status = true; e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)'; e.message += 'App pin code removed';
} else if (f.portOut < 1024 || f.portOut > 65535) { } else {
// Port in incorrect range
e.status = true; e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)'; e.message += 'App pin code added';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
} }
// set fields with error // set fields with error
if (e.status) { if (!e.status) {
emitError(`Invalid Input: ${e.message}`); emitError(`Invalid Input: ${e.message}`);
return; } else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
} }
// Post here
postOSC(formData);
setChanged(false);
setSubmitting(false); setSubmitting(false);
}; };
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
const disableModal = status !== 'success';
return ( return (
<>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}> <ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}> <p className={style.notes}>
Options related to the application Options related to the application
<br /> <br />
🔥 Changes take effect after app restart 🔥 🔥 Changes take effect after app restart 🔥
</p> </p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>General App Settings</div>
<div className={style.modalInline}>
<FormControl id='serverPort'> <FormControl id='serverPort'>
<FormLabel htmlFor='serverPort'> <FormLabel htmlFor='serverPort'>
Viewer Port Viewer Port
<span className={style.notes}>Port to access viewers</span> <span className={style.labelNote}>
<br />
Ontime is available at port
</span>
</FormLabel> </FormLabel>
<Input <Input
size='sm' {...inputProps}
name='title' name='title'
placeholder='4001'
autoComplete='off'
value={4001} value={4001}
readOnly disabled
style={{ width: '6em', textAlign: 'center' }}
/>
<span className={style.notes}>(Read Only Value)</span>
</FormControl>
<FormControl id='port'>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.notes}>
<br />
App Control - Default 8888
</span>
</FormLabel>
<Input
size='sm'
name='port'
placeholder='8888'
autoComplete='off'
type='number'
value={formData.port}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
port: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'center' }} style={{ width: '6em', textAlign: 'center' }}
/> />
</FormControl> </FormControl>
<div className={style.modalInline}> <FormControl id='editorPin'>
<FormControl id='targetIP' width='auto'> <FormLabel htmlFor='editorPin'>
<FormLabel htmlFor='targetIP'> Editor Pincode
OSC Out Target IP <span className={style.labelNote}>
<span className={style.notes}>
<br /> <br />
App Feedback - Default 127.0.0.1 Protect the editor with a Pincode
</span> </span>
</FormLabel> </FormLabel>
<Input <div className={style.pin}>
size='sm' <PinInput
name='targetIP' {...inputProps}
placeholder='127.0.0.1' type='alphanumeric'
autoComplete='off' defaultValue=''
value={formData.targetIP} value={formData.pinCode}
onChange={(event) => { mask={hidePin}
setChanged(true); isDisabled={disableModal}
setFormData({ onChange={(value) => handleChange('pinCode', value)}
...formData,
targetIP: event.target.value,
});
}}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut' width='auto'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.notes}>
<br />
Default 9999
</span>
</FormLabel>
<Input
size='sm'
name='portOut'
placeholder='9999'
autoComplete='off'
type='number'
value={formData.portOut}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
portOut: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
> >
Save <PinInputField />
</Button> <PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
size='sm'
colorScheme='blue'
variant='ghost'
icon={<FiEye />}
aria-label='Editor pin code'
onMouseDown={() => setHidePin(false)}
onMouseUp={() => setHidePin(true)}
isDisabled={disableModal}
/>
</div> </div>
</ModalBody> </FormControl>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form> </form>
</> </ModalBody>
); );
} }
@@ -1,31 +1,26 @@
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { import { FormLabel, Input, Textarea } from '@chakra-ui/react';
FormLabel,
FormControl,
Input,
Button,
Textarea,
} from '@chakra-ui/react';
import { fetchEvent, postEvent } from 'app/api/eventApi'; import { fetchEvent, postEvent } from 'app/api/eventApi';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants'; import { EVENT_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { eventPlaceholderSettings } from '../../app/api/ontimeApi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function SettingsModal() { export default function SettingsModal() {
const { data, status } = useFetch(EVENT_TABLE, fetchEvent); const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState(eventPlaceholderSettings);
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
});
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => { useEffect(() => {
if (data == null) return; if (data == null) return;
if (changed) return;
setFormData({ setFormData({
title: data.title, title: data.title,
@@ -34,8 +29,11 @@ export default function SettingsModal() {
backstageInfo: data.backstageInfo, backstageInfo: data.backstageInfo,
endMessage: data.endMessage, endMessage: data.endMessage,
}); });
}, [data]); }, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => { const submitHandler = async (event) => {
event.preventDefault(); event.preventDefault();
setSubmitting(true); setSubmitting(true);
@@ -46,133 +44,128 @@ export default function SettingsModal() {
setSubmitting(false); setSubmitting(false);
}; };
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return ( return (
<>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}> <ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}> <p className={style.notes}>
Options related to the running event Options related to the running event
<br /> <br />
Affect rendered views Affects rendered views
</p> </p>
<form onSubmit={submitHandler}>
<FormControl id='title'> <div className={style.modalFields}>
<div className={style.hSeparator}>Event Data</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='title'>Event Title</FormLabel> <FormLabel htmlFor='title'>Event Title</FormLabel>
<Input <Input
size='sm' {...inputProps}
maxLength={35} maxLength={35}
name='title' name='title'
placeholder='Event Title' placeholder='Event Title'
autoComplete='off'
value={formData.title} value={formData.title}
onChange={(event) => { onChange={(event) => handleChange('title', event.target.value)}
setChanged(true);
setFormData({ ...formData, title: event.target.value });
}}
isDisabled={submitting}
/> />
</FormControl> </div>
<div className={style.hSeparator}>Additional Screen Info</div>
<FormControl id='url'> <div className={style.spacedEntry}>
<FormLabel htmlFor='url'> <FormLabel htmlFor='url'>
Event URL Event URL
<span className={style.notes}> <span className={style.labelNote}>
(shown as a QR code in some views) <br />
Shown as a QR code in some views
</span> </span>
</FormLabel> </FormLabel>
<Input <Input
size='sm' {...inputProps}
name='url' name='url'
placeholder='www.onsite.no' placeholder='www.onsite.no'
autoComplete='off'
value={formData.url} value={formData.url}
onChange={(event) => { onChange={(event) => handleChange('url', event.target.value)}
setChanged(true);
setFormData({ ...formData, url: event.target.value });
}}
isDisabled={submitting}
/> />
</FormControl> </div>
<div className={style.spacedEntry}>
<FormControl id='pubInfo'> <FormLabel htmlFor='pubInfo'>
<FormLabel htmlFor='pubInfo'>Public Info</FormLabel> Public Info
<span className={style.labelNote}>
<br />
Information to be shown on public screens
</span>
</FormLabel>
<Textarea <Textarea
size='sm' {...inputProps}
name='pubInfo' name='pubInfo'
placeholder='Information to be shown on public screens' placeholder='Information to be shown on public screens'
autoComplete='off'
value={formData.publicInfo} value={formData.publicInfo}
onChange={(event) => { onChange={(event) =>
setChanged(true); handleChange('publicInfo', event.target.value)
setFormData({ }
...formData,
publicInfo: event.target.value,
});
}}
isDisabled={submitting}
/> />
</FormControl> </div>
<div className={style.spacedEntry}>
<FormControl id='backstageInfo'> <FormLabel htmlFor='backstageInfo'>
<FormLabel htmlFor='backstageInfo'>Backstage Info</FormLabel> Backstage Info
<span className={style.labelNote}>
<br />
Information to be shown on backstage screens
</span>
</FormLabel>
<Textarea <Textarea
size='sm' {...inputProps}
name='backstageInfo' name='backstageInfo'
placeholder='Information to be shown on backstage screens' placeholder='Information to be shown on backstage screens'
autoComplete='off'
resize={false} resize={false}
value={formData.backstageInfo} value={formData.backstageInfo}
onChange={(event) => { onChange={(event) =>
setChanged(true); handleChange('backstageInfo', event.target.value)
setFormData({ }
...formData,
backstageInfo: event.target.value,
});
}}
isDisabled={submitting}
/> />
</FormControl> </div>
<div className={style.spacedEntry}>
<FormControl id='endMessage'>
<FormLabel htmlFor='endMessage'> <FormLabel htmlFor='endMessage'>
End Message End Message
<span className={style.notes}> <span className={style.labelNote}>
<br />
Shown on presenter view when time is finished Shown on presenter view when time is finished
</span> </span>
</FormLabel> </FormLabel>
<Input <Input
size='sm' {...inputProps}
maxLength={30} maxLength={30}
name='endMessage' name='endMessage'
placeholder='Empty message shows elapsed time' placeholder='Empty message shows elapsed time'
autoComplete='off'
value={formData.endMessage} value={formData.endMessage}
onChange={(event) => { onChange={(event) =>
setChanged(true); handleChange('endMessage', event.target.value)
setFormData({ }
...formData,
endMessage: event.target.value,
});
}}
isDisabled={submitting}
/> />
</FormControl>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
>
Save
</Button>
</div> </div>
</ModalBody> </div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form> </form>
</> </ModalBody>
); );
} }
@@ -1,11 +1,5 @@
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
FormLabel,
FormControl,
Input,
Button,
Switch,
} from '@chakra-ui/react';
import { import {
getInfo, getInfo,
httpPlaceholder, httpPlaceholder,
@@ -17,23 +11,23 @@ import { useFetch } from 'app/hooks/useFetch';
import { APP_TABLE } from 'app/api/apiConstants'; import { APP_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import { FiInfo } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function IntegrationSettingsModal() { export default function IntegrationSettingsModal() {
const { data, status } = useFetch(APP_TABLE, getInfo); const { data, status, refetch } = useFetch(APP_TABLE, getInfo);
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(httpPlaceholder); const [formData, setFormData] = useState(httpPlaceholder);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const ready = status === 'success'; /**
const integrationInputProps = { * Set formdata from server state
size: 'sm', */
autoComplete: 'off',
isDisabled: submitting || !ready,
};
useEffect(() => { useEffect(() => {
if (data == null) return; if (data == null) return;
if (changed) return;
setFormData({ setFormData({
onLoad: data?.onLoad, onLoad: data?.onLoad,
@@ -42,8 +36,11 @@ export default function IntegrationSettingsModal() {
onPause: data?.onPause, onPause: data?.onPause,
onStop: data?.onStop, onStop: data?.onStop,
}); });
}, [data]); }, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => { const submitHandler = async (event) => {
event.preventDefault(); event.preventDefault();
@@ -54,58 +51,80 @@ export default function IntegrationSettingsModal() {
if (e.status) { if (e.status) {
emitError(`Invalid Input: ${e.message}`); emitError(`Invalid Input: ${e.message}`);
return; return;
} else {
await postInfo(f);
setChanged(false);
} }
// Post here
postInfo(f);
setChanged(false);
setSubmitting(false); setSubmitting(false);
}; };
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
// Todo: make change handler
// Todo: toggle between GET / POST
// Todo: add test button
// Todo: enabled should be button
// Todo: add friendly placeholder to input
return ( return (
<> <ModalBody className={style.modalBody}>
<form onSubmit={submitHandler}>
<ModalBody
className={ready ? style.modalBody : style.modalBodyDisabled}
>
<>
<p className={style.notes}> <p className={style.notes}>
Integrate with third party over an HTTP API Integrate with third party over an HTTP API
<br /> <br />
🔥 Changes take effect after app restart 🔥 🔥 Changes take effect after app restart 🔥
</p> </p>
<div className={style.highNotes}> <form onSubmit={submitHandler}>
<p> <div className={style.modalFields}>
Add HTTP messages that ontime will send during the app lifecycle <div className={style.hSeparator}>Ontime event cycle</div>
</p> <div className={style.blockNotes}>
<p> <span className={style.inlineFlex}>
You can use the variables below to pass data directly from <FiInfo color='#2b6cb0' fontSize={'2em'} />
ontime eg: Add HTTP messages that ontime will send during the event cycle
<span className={style.emNote}> </span>
http://127.0.0.1:8088/API/?setHeadline=<b>$title</b> <span className={style.labelNote}>
&setSub=<b>$presenter</b> You can use variables in the HTTP request URL to send data from
ontime
</span>
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=
<span className={style.labelNoteInline}>$title</span>
&setSub=<span className={style.labelNoteInline}>$presenter</span>
</span> </span>
</p>
<table> <table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Variable
</td>
<td className={style.labelNote}>Value</td>
</tr>
{ontimeVars.map((v) => ( {ontimeVars.map((v) => (
<tr> <tr>
<td className={style.noteItem}>{v.name}</td> <td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td> <td>{v.description}</td>
</tr> </tr>
))} ))}
</tbody>
</table> </table>
</div> </div>
<div className={style.hSeparator}>Send HTTP</div>
<> <FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel>
On Load On Load
<span className={style.notes}>When a new event loads</span> <span className={style.labelNote}>
<br />
When a new event loads
</span>
</FormLabel> </FormLabel>
<FormControl id='onLoad' className={style.modalInline}> <div className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onLoadURL' name='onLoadURL'
value={formData?.onLoad?.url} value={formData?.onLoad?.url}
onChange={(event) => { onChange={(event) => {
@@ -134,16 +153,17 @@ export default function IntegrationSettingsModal() {
}); });
}} }}
/> />
</FormControl> </div>
<FormLabel> <FormLabel style={{paddingLeft:'0.5em'}}>
On Start On Start
<span className={style.notes}> <span className={style.labelNote}>
When an timer starts / resumes <br />
When an timer starts / resumes{' '}
</span> </span>
</FormLabel> </FormLabel>
<FormControl id='onStart' className={style.modalInline}> <div className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onStartURL' name='onStartURL'
value={formData?.onStart?.url} value={formData?.onStart?.url}
onChange={(event) => { onChange={(event) => {
@@ -172,14 +192,17 @@ export default function IntegrationSettingsModal() {
}); });
}} }}
/> />
</FormControl> </div>
<FormLabel> <FormLabel style={{paddingLeft:'0.5em'}}>
On Update On Update
<span className={style.notes}>At every clock tick</span> <span className={style.labelNote}>
<br />
At every clock tick
</span>
</FormLabel> </FormLabel>
<FormControl id='onUpdate' className={style.modalInline}> <FormControl id='onUpdate' className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onUpdateURL' name='onUpdateURL'
value={formData?.onUpdate?.url} value={formData?.onUpdate?.url}
onChange={(event) => { onChange={(event) => {
@@ -209,13 +232,16 @@ export default function IntegrationSettingsModal() {
}} }}
/> />
</FormControl> </FormControl>
<FormLabel> <FormLabel style={{paddingLeft:'0.5em'}}>
On Pause On Pause
<span className={style.notes}>When a timer pauses</span> <span className={style.labelNote}>
<br />
When a timer pauses
</span>
</FormLabel> </FormLabel>
<FormControl id='onPause' className={style.modalInline}> <FormControl id='onPause' className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onPauseURL' name='onPauseURL'
value={formData?.onPause?.url} value={formData?.onPause?.url}
onChange={(event) => { onChange={(event) => {
@@ -245,13 +271,16 @@ export default function IntegrationSettingsModal() {
}} }}
/> />
</FormControl> </FormControl>
<FormLabel> <FormLabel style={{paddingLeft:'0.5em'}}>
On Stop On Stop
<span className={style.notes}>When an event is unloaded</span> <span className={style.labelNote}>
<br />
When an event is unloaded
</span>
</FormLabel> </FormLabel>
<FormControl id='onStop' className={style.modalInline}> <FormControl id='onStop' className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onStopURL' name='onStopURL'
value={formData?.onStop?.url} value={formData?.onStop?.url}
onChange={(event) => { onChange={(event) => {
@@ -281,13 +310,16 @@ export default function IntegrationSettingsModal() {
}} }}
/> />
</FormControl> </FormControl>
<FormLabel> <FormLabel style={{paddingLeft:'0.5em'}}>
On Finish On Finish
<span className={style.notes}>When an event is finished</span> <span className={style.labelNote}>
<br />
When an event is finished
</span>
</FormLabel> </FormLabel>
<FormControl id='onFinish' className={style.modalInline}> <FormControl id='onFinish' className={style.modalInline}>
<Input <Input
{...integrationInputProps} {...inputProps}
name='onFinishURL' name='onFinishURL'
value={formData?.onFinish?.url} value={formData?.onFinish?.url}
onChange={(event) => { onChange={(event) => {
@@ -317,20 +349,14 @@ export default function IntegrationSettingsModal() {
}} }}
/> />
</FormControl> </FormControl>
</>
</>
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || !ready}
>
Save
</Button>
</div> </div>
</ModalBody> <SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form> </form>
</> </ModalBody>
); );
} }
+12 -6
View File
@@ -8,9 +8,10 @@ import {
} from '@chakra-ui/modal'; } from '@chakra-ui/modal';
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
import EventSettingsModal from './EventSettingsModal'; import EventSettingsModal from './EventSettingsModal';
import AppSettingsModal from './AppSettingsModal'; import OscSettingsModal from './OscSettingsModal';
import AliasesModal from './AliasesModal'; import AliasesModal from './AliasesModal';
import IntegrationSettingsModal from './IntegrationSettingsModal'; import IntegrationSettingsModal from './IntegrationSettingsModal';
import AppSettingsModal from './AppSettingsModal';
export default function ModalManager(props) { export default function ModalManager(props) {
const { isOpen, onClose } = props; const { isOpen, onClose } = props;
@@ -20,7 +21,8 @@ export default function ModalManager(props) {
onClose={onClose} onClose={onClose}
closeOnOverlayClick={false} closeOnOverlayClick={false}
motionPreset={'slideInBottom'} motionPreset={'slideInBottom'}
size='lg' size='xl'
scrollBehavior='inside'
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
@@ -29,21 +31,25 @@ export default function ModalManager(props) {
<Tabs size='sm' isLazy> <Tabs size='sm' isLazy>
<TabList> <TabList>
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab> <Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
<Tab style={{ fontSize: '0.9em' }}>Application Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab> <Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/} {/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel> <TabPanel>
<AppSettingsModal /> <AppSettingsModal />
</TabPanel> </TabPanel>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel> <TabPanel>
<AliasesModal /> <AliasesModal />
</TabPanel> </TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
{/*<TabPanel>*/} {/*<TabPanel>*/}
{/* <IntegrationSettingsModal />*/} {/* <IntegrationSettingsModal />*/}
{/*</TabPanel>*/} {/*</TabPanel>*/}
+108 -31
View File
@@ -1,41 +1,101 @@
@use '../../styles/variables' as *; @use '../../styles/main' as *;
//////////////////////////////////// main //////////////////////////////////// main
.modalBody, .modalBody {
.modalBodyDisabled {
font-weight: 400; font-weight: 400;
.notes { .notes {
font-weight: 400; font-weight: 400;
color: $light-bg; color: $light-bg;
display: grid;
place-items: center;
height: 4em;
} }
.separator { .modalFields {
border: 1px solid $light-bg-transparent; min-height: 45vh;
width: 50%; max-height: 45vh;
margin: 0.5em auto; overflow-y: auto;
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
padding-right: 6px;
label {
//font-weight: 400;
font-size: 0.8em;
}
.inlineAlias,
.inlineAliasPlaceholder {
display: grid;
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
gap: 8px;
align-items: center;
}
.error {
font-size: 0.8em;
color: $error-red;
}
.inlineAliasPlaceholder {
grid-template-columns: 20% 1fr 4em;
.placeholder {
background: $light-text;
width: 100%;
height: 24px;
}
}
}
/* Track */
::-webkit-scrollbar-track {
background: rgba($light-bg, 0.15);
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba($light-bg, 0.35);
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba($light-bg, 0.45);
} }
.modalInline { .modalInline {
display: flex; display: flex;
gap: 2em; gap: 2em;
align-items: center; align-items: center;
padding: 0 0.5em 0.5em 0.5em;
}
.spacedEntry {
padding: 0 0.5em 0.5em 0.5em;
}
.pin {
display: flex;
gap: 0.5em;
border-radius: 50%;
input {
border-radius: 50%;
}
} }
.submitContainer { .submitContainer {
margin-top: 2em;
display: flex; display: flex;
flex-direction: row-reverse; justify-content: flex-end;
gap: 1em;
button {
margin-top: 1em;
}
} }
} }
.modalBody > *, .modalBody > * {
.modalBodyDisabled > * {
margin-top: 0.5em; margin-top: 0.5em;
} }
@@ -52,32 +112,44 @@ p {
span { span {
&.notes { &.notes {
font-size: 0.8em; font-size: 0.9em;
padding-left: 0.4em; padding-left: 0.4em;
} }
} }
.highNotes { .blockNotes {
background-color: $light-text; background-color: $bg-gray;
margin: 1em 0; margin: 1em 0;
padding: 0.3em; padding: 0.5em;
font-size: 0.9em; font-size: 0.8em;
border-radius: 2px;
table { table {
background-color: $light-text; background-color: #fff;
border-left: 4px solid lighten($ontime-pink, 5%);
width: 100%; width: 100%;
margin-top: 0.3em; margin: 0.5em 0;
border-radius: 2px;
:first-child {
padding-left: 1em;
}
td {
user-select: text;
}
} }
.noteItem { .noteItem {
user-select: text; user-select: text;
font-weight: 700; font-weight: 600;
padding-right: 2em; padding-right: 2em;
} }
.flexNote { .flexNote {
user-select: text; user-select: text;
padding-bottom: 0.3em; padding-bottom: 0.3em;
display: block;
} }
.emNote { .emNote {
@@ -87,13 +159,18 @@ span {
} }
} }
// Define style for a link .labelNote {
a { color: $light-bg;
&::after { padding-right: 1em;
content: ' \2197'; }
color: $accent;
} .labelNoteInline {
&:hover { color: $light-bg;
color: $accent; }
}
.inlineFlex {
display: flex;
gap: 1em;
align-items: center;
margin-bottom: 1em;
} }
@@ -0,0 +1,168 @@
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import SubmitContainer from './SubmitContainer';
import { inputProps, portInputProps } from './modalHelper';
export default function OscSettingsModal() {
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({ ...data });
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
let e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
}
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
setChanged(false);
}
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {(string | number)} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to Open Sound Control
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>OSC Input (control)</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.labelNote}>
<br />
Open port for 3rd party control over OSC - Default 8888
</span>
</FormLabel>
<Input
{...portInputProps}
name='port'
placeholder='8888'
value={formData.port}
onChange={(event) =>
handleChange('port', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'center' }}
/>
</div>
<div className={style.hSeparator}>OSC Output (feedback)</div>
<div className={style.modalInline}>
<FormControl id='targetIP'>
<FormLabel htmlFor='targetIP'>
OSC Out Target IP
<span className={style.labelNote}>
<br />
Default 127.0.0.1
</span>
</FormLabel>
<Input
{...inputProps}
size='sm'
name='targetIP'
placeholder='127.0.0.1'
autoComplete='off'
value={formData.targetIP}
onChange={(event) =>
handleChange('targetIP', event.target.value)
}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.labelNote}>
<br />
Default 9999
</span>
</FormLabel>
<Input
{...portInputProps}
name='portOut'
placeholder='9999'
value={formData.portOut}
onChange={(event) =>
handleChange('portOut', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</ModalBody>
);
}
@@ -0,0 +1,35 @@
import style from './Modals.module.scss';
import { Button } from '@chakra-ui/button';
import PropTypes from 'prop-types';
export default function SubmitContainer(props) {
const { submitting, changed, revert, status } = props;
return (
<div className={style.submitContainer}>
<Button
type='submit'
isDisabled={submitting || !changed}
variant='ghosted'
onClick={() => revert()}
>
Revert
</Button>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || status !== 'success'}
>
Save
</Button>
</div>
);
}
SubmitContainer.propTypes = {
submitting: PropTypes.bool,
changed: PropTypes.bool,
status: PropTypes.string,
revert: PropTypes.func.isRequired,
};
+11
View File
@@ -0,0 +1,11 @@
export const inputProps = {
size: 'sm',
autoComplete: 'off',
};
export const portInputProps = {
...inputProps,
type: 'number',
min: '1024',
max: '65535',
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { fetchAllEvents } from 'app/api/eventsApi'; import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent } from 'app/api/eventApi'; import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'ontime-server/utils/time'; import { stringFromMillis } from 'ontime-utils/time';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants'; import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
@@ -1,6 +1,7 @@
import { memo, useEffect, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import LowerClean from './LowerClean'; import LowerClean from './LowerClean';
import LowerLines from './LowerLines'; import LowerLines from './LowerLines';
import { useSearchParams } from 'react-router-dom';
const isEqual = require('react-fast-compare'); const isEqual = require('react-fast-compare');
const areEqual = (prevProps, nextProps) => { const areEqual = (prevProps, nextProps) => {
@@ -12,6 +13,7 @@ const areEqual = (prevProps, nextProps) => {
const Lower = (props) => { const Lower = (props) => {
const { title } = props; const { title } = props;
const [searchParams,] = useSearchParams();
const [titles, setTitles] = useState({ const [titles, setTitles] = useState({
titleNow: '', titleNow: '',
titleNext: '', titleNext: '',
@@ -61,61 +63,59 @@ const Lower = (props) => {
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5 // eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
// Check for user options // Check for user options
useEffect(() => { useEffect(() => {
// get parameters
const params = new URLSearchParams(props.location.search);
// create aux // create aux
let options = {}; let options = {};
// preset: selector // preset: selector
// Should be a number 1-n // Should be a number 1-n
let p = parseInt(params.get('preset')); let p = parseInt(searchParams.get('preset'));
if (!isNaN(p)) setPreset(p); if (!isNaN(p)) setPreset(p);
// size: multiplier // size: multiplier
// Should be a number 0.0-n // Should be a number 0.0-n
let s = params.get('size'); let s = searchParams.get('size');
if (s) options.size = s; if (s) options.size = s;
// transitionIn: seconds // transitionIn: seconds
// Should be a number 0-n // Should be a number 0-n
let t = parseInt(params.get('transition')); let t = parseInt(searchParams.get('transition'));
if (!isNaN(t)) options.transitionIn = t; if (!isNaN(t)) options.transitionIn = t;
// textColour: string // textColour: string
// Should be a hex string '#ffffff' // Should be a hex string '#ffffff'
let c = params.get('text'); let c = searchParams.get('text');
if (c) options.textColour = `#${c}`; if (c) options.textColour = `#${c}`;
// bgColour: string // bgColour: string
// Should be a hex string '#ffffff' // Should be a hex string '#ffffff'
let b = params.get('bg'); let b = searchParams.get('bg');
if (b) options.bgColour = `#${b}`; if (b) options.bgColour = `#${b}`;
// key: string // key: string
// Should be a hex string '#00FF00' with key colour // Should be a hex string '#00FF00' with key colour
let k = params.get('key'); let k = searchParams.get('key');
if (k) options.keyColour = `#${k}`; if (k) options.keyColour = `#${k}`;
// fadeOut: seconds // fadeOut: seconds
// Should be a number 0-n // Should be a number 0-n
let f = parseInt(params.get('fadeout')); let f = parseInt(searchParams.get('fadeout'));
if (!isNaN(f)) options.fadeOut = f; if (!isNaN(f)) options.fadeOut = f;
// x: pixels // x: pixels
// Should be a number 0-n // Should be a number 0-n
let x = parseInt(params.get('x')); let x = parseInt(searchParams.get('x'));
if (!isNaN(x)) options.posX = x; if (!isNaN(x)) options.posX = x;
// y: pixels // y: pixels
// Should be a number 0-n // Should be a number 0-n
let y = parseInt(params.get('y')); let y = parseInt(searchParams.get('y'));
if (!isNaN(y)) options.posY = y; if (!isNaN(y)) options.posY = y;
setLowerOptions({ setLowerOptions({
...options, ...options,
set: true, set: true,
}); });
}, [props.location.search]); }, [searchParams]);
// Defer rendering until we have data ready // Defer rendering until we have data ready
if (!lowerOptions.set) return null; if (!lowerOptions.set) return null;
+10
View File
@@ -5,16 +5,26 @@ import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { AppContextProvider } from './app/context/AppContext';
import SocketProvider from './app/context/socketContext';
// Load Open Sans typeface // Load Open Sans typeface
require('typeface-open-sans'); require('typeface-open-sans');
const queryClient = new QueryClient();
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<ChakraProvider resetCSS> <ChakraProvider resetCSS>
<SocketProvider>
<QueryClientProvider client={queryClient}>
<AppContextProvider>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</SocketProvider>
</ChakraProvider> </ChakraProvider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
+44 -1
View File
@@ -1,4 +1,22 @@
//////////////////////////////////// general app style //////////////////////////////////// general app colours
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
$bg-gray: #f4f4f8;
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$error-red: #E53E3E;
//////////////////////////////////// general app element overriders
// no decoration on lists // no decoration on lists
ul { ul {
@@ -9,3 +27,28 @@ ul {
textarea { textarea {
resize: none !important; resize: none !important;
} }
// Define style for a link
a {
&::after {
content: ' \2197';
color: $ontime-pink;
}
&:hover {
color: $ontime-pink;
}
}
// horizontal separator
.hSeparator {
width: 100%;
border-bottom: 1px solid $light-text;
margin: 1em auto;
display: flex;
align-items: center;
}
// inline vertical separator
.vSpan {
margin: 0 0.5em;
}
+9
View File
@@ -0,0 +1,9 @@
//////////////////////////////////// general app elements
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
-4
View File
@@ -1,4 +0,0 @@
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$accent: #ff7597;
+232 -282
View File
@@ -1786,7 +1786,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.13.10" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
@@ -1800,7 +1800,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.16.3": "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6":
version "7.16.5" version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA== integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
@@ -1877,24 +1877,24 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@chakra-ui/accordion@1.4.1": "@chakra-ui/accordion@1.4.2":
version "1.4.1" version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.1.tgz#bd5373dcfd11572e5e2c535eca51aee4fc24c828" resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.2.tgz#e851d5e0f748646e20efde909b5f70a93679283b"
integrity sha512-/E0FW5YHNVD6WwMGiuQuXpA70P2CKAV+MzcMITnSGPWsh9XD0mcXvMkIALVojfFk9tcCFdIGnxX/HWr41LzgIg== integrity sha512-BAGMvcm2sFE5Ft7jwC9nF03/Yv7qztuhzwKBBy4iL0p1nCPh6kV54RBXUcoj3VWe+yrmNiAVYKRTdqQBTJFwOw==
dependencies: dependencies:
"@chakra-ui/descendant" "2.1.1" "@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1" "@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/alert@1.3.1": "@chakra-ui/alert@1.3.2":
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.1.tgz#d091b36ee4a7e3684220cd6f86fd2e08ca71cb2a" resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.2.tgz#6c038b0bd58f3964e0a3af213c162c108176a8b0"
integrity sha512-BeR6l/1CLZarA3uAe+5Q3hioYf7SixYfy9rOte/29ck1lx9PLjjuPYYmuDPtZNbGibhUCh48z4U/uK2x8mbpKQ== integrity sha512-+OMeVeGtydpj6nry0zH7qFDt36zEaxckRnufx1BGiCfWdUg6ahVwKXl8qX93Q8w82od7eAoBKMgGJz7IVL5NPw==
dependencies: dependencies:
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -1950,18 +1950,18 @@
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/close-button@1.2.1": "@chakra-ui/close-button@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.1.tgz#1109665e73cddcb643bd66e208b2aac75e8af40f" resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.2.tgz#43e91b34d51f75dc08ff9a56cbfc7ed5ad158b8a"
integrity sha512-A/cuFtJPF8rp5p6tCIGlQdHB89gLCSOzxWssoTXAGJnmlwY2YunFHxgkYZXwPbDqFrM8ndya7Ys+AuL1JZsa3g== integrity sha512-SqeLib0qgMjK3OsO1g5OnAHUmdCC8GMjToNEea7TeSrA44bH9EXVhFTkMMu2PnDVHbQmi4Ee1cuulNJt0UhQ3g==
dependencies: dependencies:
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/color-mode@1.3.1": "@chakra-ui/color-mode@1.3.2":
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.1.tgz#08402d28a9c5b4dd3e1a64f69faad319d55dc9d6" resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.2.tgz#e87f9c17ae891c426fa8c9dbdd3c2ba80669fa48"
integrity sha512-UuuYMfYxaBE5fPtIR6dwIJn3snT0tVX67RN2RHv4/LQgUeiddq4VziXT3bd+tyIe9+zcsqrZhV7J7YP0h/Id/Q== integrity sha512-/rWcbrzbaWCyyUnT07Qjz0xf/ltHS31CHOKtVCWr2uTgfn2gOQpdxsKRbjrLYPOYZGTMdINUHNiAsqQjLoAoTQ==
dependencies: dependencies:
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-env" "1.1.1" "@chakra-ui/react-env" "1.1.1"
@@ -2011,13 +2011,13 @@
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
react-focus-lock "2.5.2" react-focus-lock "2.5.2"
"@chakra-ui/form-control@1.5.1": "@chakra-ui/form-control@1.5.2":
version "1.5.1" version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.1.tgz#235267088026690d6994fa406c9f2618fa0351ec" resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.2.tgz#4ce34f03165cfafacecabdce3be82011b74308a7"
integrity sha512-ASZYQFOs5mAoaNXAN/ZaesMy3XV07F0/Eba5PQ7Dejdn91aep6lqF889hmr8yqcR646xCOY7ISyYsskfh9QHrQ== integrity sha512-uWv0/f+JEM0ZE5Hnj3TzCnJ09EB+A+DSs9QgyECOuxx9Ju6gnns2uaRki2BfxksQL9ZZomPCkMtXazY9Wa81ag==
dependencies: dependencies:
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2031,10 +2031,10 @@
compute-scroll-into-view "1.0.14" compute-scroll-into-view "1.0.14"
copy-to-clipboard "3.3.1" copy-to-clipboard "3.3.1"
"@chakra-ui/icon@1.2.1": "@chakra-ui/icon@2.0.0":
version "1.2.1" version "2.0.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-1.2.1.tgz#b47c0016531da8bef81061d2b96a2d5282b07ff8" resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-2.0.0.tgz#a2468736117139f94c6ed65eb86b9838e554c90e"
integrity sha512-uZxFsiY4Tld+LvGIX7cky0H6oMRac8udPMQRzIk/UQeNZcsWisGetatbQsew3y1lWV/iH/8+TlDuW13GWGyGGQ== integrity sha512-/GuU+xIcOIy9uSUUUCu249ZJB/nLDbjWGkfpoSdBwqT4+ytJrKt+0Ckh3Ub14sz3BJD+Z6IiIt6ySOA9+7lbsA==
dependencies: dependencies:
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2046,21 +2046,21 @@
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/input@1.3.1": "@chakra-ui/input@1.3.2":
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.1.tgz#a407242239f7bb5fdf3b237ab90355c7d77aff9e" resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.2.tgz#59d49ab5c8645c51cd591d46ca23d5cb95e03392"
integrity sha512-Z+LqkwVPMeUBuvB9dLDPKkBnWV52Q1PVl3KW9ouDIFg7SoemeYkBt3p4ttEKE+eIPsPlrcH1u2A/RGcCTZOe1g== integrity sha512-VMxmQgFiQ2UnBlkgLX/336G0IfYfw8YWF2ZoEFj5WL9kDSrrL1FXSBgjFGxrper74G4W20tESBCfU1S891y6cg==
dependencies: dependencies:
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/layout@1.5.1": "@chakra-ui/layout@1.6.0":
version "1.5.1" version "1.6.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.5.1.tgz#be33e8fb5fb187f13c5c0ef66fe94cd293094da6" resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.6.0.tgz#fa45d72420f01f4f6082f5a5a0c202bf7b09a059"
integrity sha512-nKiyZ5adjNTbBV3oFIUGIPijwutO1NGdev1jHtnZc3xo2urCIkBvKU8+mVjlX04IwZ7oLKoP3EiDDv0g7+o41Q== integrity sha512-WUfQ104y1wNueU33/hPlZsMzYJGjO0dXMpVkQf5ZNhNX3IGDO+5+MO2x2xloP+j45yNPi3p8ti/HBnm3dXI+3Q==
dependencies: dependencies:
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2071,51 +2071,51 @@
dependencies: dependencies:
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/media-query@1.2.1": "@chakra-ui/media-query@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.1.tgz#c1df05cbe8bd909a410d134240f9bceee760ec09" resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.2.tgz#46db9059de1367a3aa85c1fb973a78f95fca6cc5"
integrity sha512-Ho/qiPGTjNukFTE9WBdYV9FIXU7KFTJPqdRQPWANkz+j275n6sqSE1j5LRJllP+ett21KeuWLN4zL33pP0Ox+g== integrity sha512-xSmDVleE1drWiGH/MX3RqyVm29x/8Vf6G0UGaI2kCpbNmon+Q1zHW/yDHvptIuctLrPHYO8LOBxuUjfnIXwC2g==
dependencies: dependencies:
"@chakra-ui/react-env" "1.1.1" "@chakra-ui/react-env" "1.1.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/menu@1.8.1": "@chakra-ui/menu@1.8.2":
version "1.8.1" version "1.8.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.1.tgz#ad26e12843914747de1c36228f4f45c2be668a4d" resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.2.tgz#849f51beef6220667eb47c831ddf2c65919ded0e"
integrity sha512-fgzzFukBj4sQzTRf4q/+nHiVTKhrMtJdofnluqce/SCRJ1G+bbovUySblTzfI8iFlTSZt/eWc/Nju4JB1S+3Yg== integrity sha512-u2GfkwTqbWa8L/7i/kOFbU3JANiT2HStR+gsYKuiuOPiuBcUb8OlgfJfP70OtVKegNKmVEMjvzXtld3wCCo/1g==
dependencies: dependencies:
"@chakra-ui/clickable" "1.2.1" "@chakra-ui/clickable" "1.2.1"
"@chakra-ui/descendant" "2.1.1" "@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1" "@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1" "@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/modal@1.10.1": "@chakra-ui/modal@1.10.2":
version "1.10.1" version "1.10.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.1.tgz#849243b6f700845ef04e8247d3326e3e0324b789" resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.2.tgz#578c30812c63863bef25f4b5c279cacbb34528ec"
integrity sha512-cboC2ITm+5FjhrBc6yJ5cW4VXnfwlLhFa1EkPqF1k4kvYGyUHArvPN1q8AiPYOIrupHYu2Iu6YmQPg7TJwNImg== integrity sha512-ZlmYetPHwHW4CAM09j4/+Ui54dXR1nzU6mOwhWe4/IzLvEyoEU6fHJeKyGxVUpYTG/7wltG/wKFRJpYa77tiBg==
dependencies: dependencies:
"@chakra-ui/close-button" "1.2.1" "@chakra-ui/close-button" "1.2.2"
"@chakra-ui/focus-lock" "1.2.1" "@chakra-ui/focus-lock" "1.2.1"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1" "@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1" "@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
aria-hidden "^1.1.1" aria-hidden "^1.1.1"
react-remove-scroll "2.4.1" react-remove-scroll "2.4.1"
"@chakra-ui/number-input@1.3.1": "@chakra-ui/number-input@1.3.2":
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.1.tgz#1c46b2748240c780617955da4f2f20a5414edf5c" resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.2.tgz#580035e56a13c30b04034ab02cb9c1132a1cf09c"
integrity sha512-4vBRSShT5pedElgP9YGVC+9RHzQGmUVZqu3p0gZW0fLGVVQ9C1EGrO7djL+k3tgklyu8RvSwkRDJqEPvbQKDgQ== integrity sha512-7x7AoqwPXU1odyDcqIwjBwf0MJUwYMM2fa+6YZ52F941GKlvkDiiJOhK6xfhhBzkLUQD6DN8zgAmmGhaZ6UQXw==
dependencies: dependencies:
"@chakra-ui/counter" "1.2.1" "@chakra-ui/counter" "1.2.1"
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2129,12 +2129,12 @@
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/popover@1.10.1": "@chakra-ui/popover@1.11.0":
version "1.10.1" version "1.11.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.10.1.tgz#03a172201d46bde73eeff7e40e141d82876df493" resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.11.0.tgz#3ad9faf26019c750a5f8c619ee80dd97bcc4ee3e"
integrity sha512-/dMUQfd+h9j3GBtkA/nYaQ5xeu4vk0psUClFvLOAJRwXGN3aMrzn/mhrvHWQ/cJuwQrO1WzxH2+g6pwsFOm9ng== integrity sha512-cCHXAfhIRir+M0ehlYIjDw3mHpiCxDTJ9WV0H1zHQV8nDYVIlZw3nEntaq8oJrv0wpIzq2WCW5ss+bBR7nLZ1A==
dependencies: dependencies:
"@chakra-ui/close-button" "1.2.1" "@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1" "@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
@@ -2165,24 +2165,24 @@
"@chakra-ui/theme-tools" "1.3.1" "@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/provider@1.7.1": "@chakra-ui/provider@1.7.3":
version "1.7.1" version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.1.tgz#d49c91abd91df8cd2824255f10353a7c17b8df9e" resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.3.tgz#625c0b7c1265515399bb501bc929d8851bba2958"
integrity sha512-E2z56BAYytg+sA7arxUtg54AzhYPpR5QUcR4L679aahgiod+5y6H5TSZcTnxTFRLXVLZbBbP/eH63seIcRqCRQ== integrity sha512-D1SrQ7do4yzAv9/OTF3yj/BkLm7kFo5DdeuOCyvXGpVJumnvbtjltRmC7rFQH4R+y9qXPvfQP4LKMNBqSxPNng==
dependencies: dependencies:
"@chakra-ui/css-reset" "1.1.1" "@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1" "@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-env" "1.1.1" "@chakra-ui/react-env" "1.1.1"
"@chakra-ui/system" "1.8.1" "@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/radio@1.4.1": "@chakra-ui/radio@1.4.3":
version "1.4.1" version "1.4.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.1.tgz#62de496295f30a6b264cc3906dcfd17bc85a0e85" resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.3.tgz#d87788fe688d0d58252e9efeb926d0417a70e158"
integrity sha512-Rq7goauffMNqwGVuPh4zh0kX0AuvTll66PhOwPu+GYHzsYO5+L9X875Q9QBnJEFtDjNF/MVIrH9WrKWjdWLqWw== integrity sha512-TQdyfdUD3BLklOP67n82JN8ksQv1BYjvaYsK0m6WCa0LDJr9aCC+XtUPgVq/1L2t4HqHdiGOrGBooF4vvy/+BA==
dependencies: dependencies:
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2202,81 +2202,81 @@
dependencies: dependencies:
"@chakra-ui/utils" "^1.9.1" "@chakra-ui/utils" "^1.9.1"
"@chakra-ui/react@^1.7.1": "@chakra-ui/react@^1.7.3":
version "1.7.1" version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.1.tgz#f88417edb4d08766bf102a8d8cc294cbd38443de" resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.3.tgz#358a3ff9847b78a798384db5c64cc222c9a48ae8"
integrity sha512-mbU6M/lxD9BCuXXSEcscPpWqR4mWxXyTk0vkWcgLG59lMFAZ4/Ll+rG1xRHaCuRsAndWNws8h4+NcxY0eECfLQ== integrity sha512-6mrfDUOa9MoQ44Xvi7xgdDq48jTTTjW9BupCGf2R3DI+z6RbUKIHzbcoDJZt2HGY6j9EarMVNRoQJzvzGUKpoQ==
dependencies: dependencies:
"@chakra-ui/accordion" "1.4.1" "@chakra-ui/accordion" "1.4.2"
"@chakra-ui/alert" "1.3.1" "@chakra-ui/alert" "1.3.2"
"@chakra-ui/avatar" "1.3.1" "@chakra-ui/avatar" "1.3.1"
"@chakra-ui/breadcrumb" "1.3.1" "@chakra-ui/breadcrumb" "1.3.1"
"@chakra-ui/button" "1.5.1" "@chakra-ui/button" "1.5.1"
"@chakra-ui/checkbox" "1.6.1" "@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/close-button" "1.2.1" "@chakra-ui/close-button" "1.2.2"
"@chakra-ui/control-box" "1.1.1" "@chakra-ui/control-box" "1.1.1"
"@chakra-ui/counter" "1.2.1" "@chakra-ui/counter" "1.2.1"
"@chakra-ui/css-reset" "1.1.1" "@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/editable" "1.3.1" "@chakra-ui/editable" "1.3.1"
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/image" "1.1.1" "@chakra-ui/image" "1.1.1"
"@chakra-ui/input" "1.3.1" "@chakra-ui/input" "1.3.2"
"@chakra-ui/layout" "1.5.1" "@chakra-ui/layout" "1.6.0"
"@chakra-ui/live-region" "1.1.1" "@chakra-ui/live-region" "1.1.1"
"@chakra-ui/media-query" "1.2.1" "@chakra-ui/media-query" "1.2.2"
"@chakra-ui/menu" "1.8.1" "@chakra-ui/menu" "1.8.2"
"@chakra-ui/modal" "1.10.1" "@chakra-ui/modal" "1.10.2"
"@chakra-ui/number-input" "1.3.1" "@chakra-ui/number-input" "1.3.2"
"@chakra-ui/pin-input" "1.7.1" "@chakra-ui/pin-input" "1.7.1"
"@chakra-ui/popover" "1.10.1" "@chakra-ui/popover" "1.11.0"
"@chakra-ui/popper" "2.4.1" "@chakra-ui/popper" "2.4.1"
"@chakra-ui/portal" "1.3.1" "@chakra-ui/portal" "1.3.1"
"@chakra-ui/progress" "1.2.1" "@chakra-ui/progress" "1.2.1"
"@chakra-ui/provider" "1.7.1" "@chakra-ui/provider" "1.7.3"
"@chakra-ui/radio" "1.4.1" "@chakra-ui/radio" "1.4.3"
"@chakra-ui/react-env" "1.1.1" "@chakra-ui/react-env" "1.1.1"
"@chakra-ui/select" "1.2.1" "@chakra-ui/select" "1.2.2"
"@chakra-ui/skeleton" "1.2.1" "@chakra-ui/skeleton" "1.2.3"
"@chakra-ui/slider" "1.5.1" "@chakra-ui/slider" "1.5.2"
"@chakra-ui/spinner" "1.2.1" "@chakra-ui/spinner" "1.2.1"
"@chakra-ui/stat" "1.2.1" "@chakra-ui/stat" "1.2.2"
"@chakra-ui/switch" "1.3.1" "@chakra-ui/switch" "1.3.1"
"@chakra-ui/system" "1.8.1" "@chakra-ui/system" "1.8.3"
"@chakra-ui/table" "1.3.1" "@chakra-ui/table" "1.3.1"
"@chakra-ui/tabs" "1.6.1" "@chakra-ui/tabs" "1.6.1"
"@chakra-ui/tag" "1.2.1" "@chakra-ui/tag" "1.2.2"
"@chakra-ui/textarea" "1.2.1" "@chakra-ui/textarea" "1.2.2"
"@chakra-ui/theme" "1.12.1" "@chakra-ui/theme" "1.12.2"
"@chakra-ui/toast" "1.4.1" "@chakra-ui/toast" "1.5.0"
"@chakra-ui/tooltip" "1.4.1" "@chakra-ui/tooltip" "1.4.2"
"@chakra-ui/transition" "1.4.1" "@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1" "@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/select@1.2.1": "@chakra-ui/select@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.1.tgz#a014c44efdc992ced96a4a74364148a4846c3d1f" resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.2.tgz#1c2956c21a012e7c9ea294db42d3cab0d05bed24"
integrity sha512-GqRmYGjVnw/Z/2RQiW7Ywuu9O5E0spmMUBjeE/v0rqjixBqrmdApjg5pmJ4YmUMvUI/WkGtR3FR5W9Y5PpvfKw== integrity sha512-EchJW3St1DtSWHe//DHwKjGsQYL2zbKcNCLnJWQKGMPZsQhAD2wsm4xjowFrV8AkY7jbVM/U2v68puN7YTC3hg==
dependencies: dependencies:
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/skeleton@1.2.1": "@chakra-ui/skeleton@1.2.3":
version "1.2.1" version "1.2.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.1.tgz#6083aadb3ad62e8d63eb64d3985b00a1fdcd9746" resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.3.tgz#f7be28e3af214473fb6cdec45d0715a049f82575"
integrity sha512-08yOmINorbxtv1xgZpHiTE7YCYdicZ5dCkc7nCVxvYQihH5efkEiLbP3EefZzFCTqa2KFN8BvgD5eA76TqleXA== integrity sha512-u5ASkzPiBjfvKxKuBienUfmyYDTHziSWQ8Ny6k83LbwLv9IcmBNGsSkmsp7hesgi9cMHGBQ3hY2GTqG9ljndIg==
dependencies: dependencies:
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/media-query" "1.2.1" "@chakra-ui/media-query" "1.2.2"
"@chakra-ui/system" "1.8.1" "@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/slider@1.5.1": "@chakra-ui/slider@1.5.2":
version "1.5.1" version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.1.tgz#2bb3ef0321d81577132a67ef2d0aa1f594d2242a" resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.2.tgz#173218b55befe0f7200fe7f332234b3824d19b08"
integrity sha512-3C0DnUapVdVq9hU1VWAsk0qc6ws1TesjQl/KTkxlP4yBJxKAja1HtyeN9c1x4jizl1a1Q7BJrYICupy70Ju76A== integrity sha512-zP07TMew61GkJe47Nu7zEg/SUEwPHpN4alW6VUM6Y8UaVpQaDx7InarbWTc/bXdTP03SfE+hQ6WD9Oy7noe4hQ==
dependencies: dependencies:
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
@@ -2290,19 +2290,19 @@
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1" "@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/stat@1.2.1": "@chakra-ui/stat@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.1.tgz#76be860aa5ae7ef66717ab7a7fcf5862268a05e7" resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.2.tgz#4a690b9a01986a838eb38ff20ee9cfaccdff0cdd"
integrity sha512-BTZFeh/8VdgUX080taCQj1g/rS4wGc+y3GQnklqlZ9N/bEv0gyLqQga7TFC/NkVl3cvjRiMnCCPj6vRih9x+Og== integrity sha512-0StsPDC56MjzhdlBl0R8wU0uwj9L1tvhQzge/ELSDn4tQDI7VovrxpFzVH0qsj7EZDwZa0BRQaSrstzWvgmJ/Q==
dependencies: dependencies:
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1" "@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/styled-system@1.14.1": "@chakra-ui/styled-system@1.15.0":
version "1.14.1" version "1.15.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.14.1.tgz#78f49de1842a3675c44b17c9f392d7172880209a" resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.15.0.tgz#fe117ace12b452379e4df69f4779395ead55917f"
integrity sha512-dgXFYZdJicsddUnPV1X7lQksgMD0z5EvwGaIh2JHJERqNRIvth/CBAnVLQQvy/xSJK5YaSEmeuVVU0veUOQcXg== integrity sha512-LnsKeiYkUuJ+NMTwueiX0Mj8CW9XAMJrJxpQm/X3GY5L5PO7Hv6wW725Ovqdy4mhG3IK7S8444FthpsDv/luHw==
dependencies: dependencies:
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
csstype "^3.0.9" csstype "^3.0.9"
@@ -2315,14 +2315,14 @@
"@chakra-ui/checkbox" "1.6.1" "@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/system@1.8.1": "@chakra-ui/system@1.8.3":
version "1.8.1" version "1.8.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.1.tgz#ce6c5af67840e20f261894c0310a0547314b262c" resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.3.tgz#bad447f4fda427fc28e234470d9fcc5a9b524d62"
integrity sha512-CFHdAjXuEDIAvNBatTBo1Tfu/HJ1LfbmjGWI4uTRfA4sDIt+cCRc/VKSfl0IKQwy70yJOMoTehZBjS8g3RTymQ== integrity sha512-6MaevsT7A2ifgOGQQCQsfvzPVd0kEXqFrX1Oxd842bawaqthmbFdo2bBTdaia/+Ivq/8Xot2uAQSbU+3NuRiUA==
dependencies: dependencies:
"@chakra-ui/color-mode" "1.3.1" "@chakra-ui/color-mode" "1.3.2"
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/styled-system" "1.14.1" "@chakra-ui/styled-system" "1.15.0"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
react-fast-compare "3.2.0" react-fast-compare "3.2.0"
@@ -2344,20 +2344,20 @@
"@chakra-ui/react-utils" "1.2.1" "@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/tag@1.2.1": "@chakra-ui/tag@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.1.tgz#2e5b7bd9895ece35d495b83bb29c34a3142cfe01" resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.2.tgz#25519544cdbc0a32b2f8f44c7a8bae3bda912cb1"
integrity sha512-O068n+qBc+CSyvpRBJ6Lwep6SydQ9UysRqw1ETF+4fJSp9dMrBp8vOcl2SVacKaCu13qdv8UdRMBxUiTz3lh7A== integrity sha512-H25y9nEyUAUdwQDND9P4mMXKf1wf9UH4A3DyP237qVKIyYBpa4aCH8eJU4dunh2yIzASB0DWcr7lsul/HAHxmg==
dependencies: dependencies:
"@chakra-ui/icon" "1.2.1" "@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/textarea@1.2.1": "@chakra-ui/textarea@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.1.tgz#cf1594ce6496817530106f70a5d787c1a9e40f59" resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.2.tgz#3e62612e4b1bc0724ba084b0b08b247ac2bbdbab"
integrity sha512-3xDsL1qQ+eY5r4GcRL4bg90vtV/xxVlw0Z3PFehFP5JW7VwXNZIRjauR/+HlOA8eYq0cF6ch2boR1GPso6rQtw== integrity sha512-DoLdKxHk0DyrQDnj1la9wjl2AW3/SK62nfWDYLAm0ouFsw1VKPw9nU+Yyj0dPruQTzI19nLaYF26i97rtnT27g==
dependencies: dependencies:
"@chakra-ui/form-control" "1.5.1" "@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/theme-tools@1.3.1", "@chakra-ui/theme-tools@^1.3.1": "@chakra-ui/theme-tools@1.3.1", "@chakra-ui/theme-tools@^1.3.1":
@@ -2368,32 +2368,32 @@
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@ctrl/tinycolor" "^3.4.0" "@ctrl/tinycolor" "^3.4.0"
"@chakra-ui/theme@1.12.1": "@chakra-ui/theme@1.12.2":
version "1.12.1" version "1.12.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.1.tgz#48a61fe5b2b0f8f422255d3049e16bb0b2dd6e60" resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.2.tgz#191e168d83e7a66bc3e65be802ad4e4e2a9bbeb5"
integrity sha512-8yDril3rSzv42eKR0x7KdnrpN1ubY0m6q37CVUADgtboJqoJwWWX2/hqkv8CX6WJf8ZwPwFL5QIwS2FPSGgi+g== integrity sha512-LVjSf16yYHD40ILrsDEd3idVQRvJSY7JY8lvTGWo2p6v+JQESWF+zXlYi9Le+TXRpZuFvJuuQ1SEvoqVwdcJ8Q==
dependencies: dependencies:
"@chakra-ui/anatomy" "1.2.1" "@chakra-ui/anatomy" "1.2.1"
"@chakra-ui/theme-tools" "1.3.1" "@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/toast@1.4.1": "@chakra-ui/toast@1.5.0":
version "1.4.1" version "1.5.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.4.1.tgz#3e3885dd039e19f8ce60446f4843e0d8ec074b24" resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.5.0.tgz#c6456c0a4f24669e16946de318ad568e24a5d497"
integrity sha512-vzQkYwnGq2nx0bOKIQ6XpJaGzUwnWKmUjcVrz9NzGwVI4g93PS7+13515R0m1NrDp30132OeDXQ+tmQwCRRe6w== integrity sha512-rTsFx/Qos5oVPN6aZMbT/wTxwZlFNSXQqrTpJYaRcRFQGzxIDDxmGkKYfPnyJjRP9i6EqynJhXEIyhMA0xO0dw==
dependencies: dependencies:
"@chakra-ui/alert" "1.3.1" "@chakra-ui/alert" "1.3.2"
"@chakra-ui/close-button" "1.2.1" "@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/theme" "1.12.1" "@chakra-ui/theme" "1.12.2"
"@chakra-ui/transition" "1.4.1" "@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@reach/alert" "0.13.2" "@reach/alert" "0.13.2"
"@chakra-ui/tooltip@1.4.1": "@chakra-ui/tooltip@1.4.2":
version "1.4.1" version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.1.tgz#21e148b7107a92abbf90c19a3eead718b875a544" resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.2.tgz#fce61c7ad4c3e6e5833487ddf75f03c774913fb9"
integrity sha512-KvTuqSqIpIgE+YNUwN7ONDRkSGR6SK9+dgSx2PfKy0Sel7UgDPVtxByuZ6tfJ9O1VTRYEdF9k+s6Gf8eRFQbNA== integrity sha512-+wyYXG8qenKkFy2YSFfOBf3rlWADnu6S9EUxP+3Rmm78unOWXDuTJWzqy2QlXs2BwoQoifaz1LVwzmMb7WLVgQ==
dependencies: dependencies:
"@chakra-ui/hooks" "1.7.1" "@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1" "@chakra-ui/popper" "2.4.1"
@@ -2402,10 +2402,10 @@
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1" "@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/transition@1.4.1": "@chakra-ui/transition@1.4.2":
version "1.4.1" version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.1.tgz#be31c02a285d182cc9c6dcd95548f8f35de64952" resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.2.tgz#de59833aa5727c0dd328b828145e4b0dabd005b3"
integrity sha512-s/VFucc6grNdP1bxw0oQLzy167gjAgyl/GiGH9nt54nioDEiSsvn70qKg7sjajNTvpoot+urQUdr4Qh+fIUFZQ== integrity sha512-S+BNmpErHlntl//uaqv0sJegzMsQms0OnJapeZaRsvZL4s1SVYrR8kMrXigkdpeh4lAUqGsLpQHPKkzaKGbBOw==
dependencies: dependencies:
"@chakra-ui/utils" "1.9.1" "@chakra-ui/utils" "1.9.1"
@@ -2454,16 +2454,16 @@
source-map "^0.5.7" source-map "^0.5.7"
stylis "^4.0.3" stylis "^4.0.3"
"@emotion/cache@^11.5.0": "@emotion/cache@^11.7.1":
version "11.5.0" version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.5.0.tgz#a5eb78cbef8163939ee345e3ddf0af217b845e62" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-mAZ5QRpLriBtaj/k2qyrXwck6yeoz1V5lMt/jfj6igWU35yYlNKs2LziXVgvH81gnJZ+9QQNGelSsnuoAy6uIw== integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
dependencies: dependencies:
"@emotion/memoize" "^0.7.4" "@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.0.3" "@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5" "@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.10" stylis "4.0.13"
"@emotion/hash@^0.8.0": "@emotion/hash@^0.8.0":
version "0.8.0" version "0.8.0"
@@ -2477,10 +2477,10 @@
dependencies: dependencies:
"@emotion/memoize" "0.7.4" "@emotion/memoize" "0.7.4"
"@emotion/is-prop-valid@^1.1.0": "@emotion/is-prop-valid@^1.1.1":
version "1.1.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.0.tgz#29ef6be1e946fb4739f9707def860f316f668cde" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134"
integrity sha512-9RkilvXAufQHsSsjQ3PIzSns+pxuX4EW8EbGeSPjZMHuMx6z/MOzb9LpqNieQX4F3mre3NWS2+X3JNRHTQztUQ== integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==
dependencies: dependencies:
"@emotion/memoize" "^0.7.4" "@emotion/memoize" "^0.7.4"
@@ -2494,15 +2494,15 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/react@^11.5.0": "@emotion/react@^11.7.1":
version "11.5.0" version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.5.0.tgz#19b5771bbfbda5e8517e948a2d9064810f0022bd" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07"
integrity sha512-MYq/bzp3rYbee4EMBORCn4duPQfgpiEB5XzrZEBnUZAL80Qdfr7CEv/T80jwaTl/dnZmt9SnTa8NkTrwFNpLlw== integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@emotion/cache" "^11.5.0" "@emotion/cache" "^11.7.1"
"@emotion/serialize" "^1.0.2" "@emotion/serialize" "^1.0.2"
"@emotion/sheet" "^1.0.3" "@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5" "@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1" hoist-non-react-statics "^3.3.1"
@@ -2518,19 +2518,19 @@
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
csstype "^3.0.2" csstype "^3.0.2"
"@emotion/sheet@^1.0.3": "@emotion/sheet@^1.1.0":
version "1.0.3" version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.3.tgz#00c326cd7985c5ccb8fe2c1b592886579dcfab8f" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-YoX5GyQ4db7LpbmXHMuc8kebtBGP6nZfRC5Z13OKJMixBEwdZrJ914D6yJv/P+ZH/YY3F5s89NYX2hlZAf3SRQ== integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/styled@^11.3.0": "@emotion/styled@^11.6.0":
version "11.3.0" version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.3.0.tgz#d63ee00537dfb6ff612e31b0e915c5cf9925a207" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
integrity sha512-fUoLcN3BfMiLlRhJ8CuPUMEyKkLEoM+n+UyAbnqGEsCd5IzKQ7VQFLtzpJOaCD2/VR2+1hXQTnSZXVJeiTNltA== integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@emotion/babel-plugin" "^11.3.0" "@emotion/babel-plugin" "^11.3.0"
"@emotion/is-prop-valid" "^1.1.0" "@emotion/is-prop-valid" "^1.1.1"
"@emotion/serialize" "^1.0.2" "@emotion/serialize" "^1.0.2"
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
@@ -5301,10 +5301,10 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
engine.io-client@~6.0.1: engine.io-client@~6.1.1:
version "6.0.2" version "6.1.1"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.0.2.tgz#ccfc059051e65ca63845e65929184757754cc34e" resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.1.1.tgz#800d4b9db5487d169686729e5bd887afa78d36b0"
integrity sha512-cAep9lhZV6Q8jMXx3TNSU5cydMzMed8/O7Tz5uzyqZvpNPtQ3WQXrLYGADxlsuaFmOLN7wZLmT7ImiFhUOku8g== integrity sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.0.0" "@socket.io/component-emitter" "~3.0.0"
debug "~4.3.1" debug "~4.3.1"
@@ -6306,19 +6306,14 @@ hey-listen@^1.0.8:
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
history@^4.9.0: history@^5.2.0:
version "4.10.1" version "5.2.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" resolved "https://registry.yarnpkg.com/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==
dependencies: dependencies:
"@babel/runtime" "^7.1.2" "@babel/runtime" "^7.7.6"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6853,11 +6848,6 @@ is-wsl@^2.2.0:
dependencies: dependencies:
is-docker "^2.0.0" is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@~1.0.0: isarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -7670,7 +7660,7 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -7832,14 +7822,6 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-create-react-context@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
dependencies:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-css-extract-plugin@^2.4.5: mini-css-extract-plugin@^2.4.5:
version "2.4.5" version "2.4.5"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef"
@@ -8133,6 +8115,10 @@ onetime@^5.1.2:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
open@^8.0.9, open@^8.4.0: open@^8.0.9, open@^8.4.0:
version "8.4.0" version "8.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
@@ -8323,13 +8309,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^4.0.0: path-type@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -9220,15 +9199,15 @@ react-icons@^4.3.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-code@^2.0.2: react-qr-code@2.0.3:
version "2.0.2" version "2.0.3"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8" resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.3.tgz#cc80785e08f817d1ab066ca4035262f77d049648"
integrity sha512-73VGe81MgeE5FJNFgdY42ez/wPPJTHuooU3iE4CX+6F8M88O1Gg4zNA0L4bKEpoySQ0QjqreJgyXjFrG/QfsdA== integrity sha512-6GDH0l53lksf2JgZwwcoS0D60a1OAal/GQRyNFkMBW19HjSqvtD5S20scmSQsKl+BgWM85Wd5DCcUYoHd+PZnQ==
dependencies: dependencies:
prop-types "^15.7.2" prop-types "^15.7.2"
qr.js "0.0.0" qr.js "0.0.0"
@@ -9278,34 +9257,20 @@ react-remove-scroll@2.4.1:
use-callback-ref "^1.2.3" use-callback-ref "^1.2.3"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-router-dom@^5.2.0: react-router-dom@^6.2.1:
version "5.2.0" version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==
dependencies: dependencies:
"@babel/runtime" "^7.1.2" history "^5.2.0"
history "^4.9.0" react-router "6.2.1"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.0, react-router@^5.2.0: react-router@6.2.1:
version "5.2.0" version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==
dependencies: dependencies:
"@babel/runtime" "^7.1.2" history "^5.2.0"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-scripts@5.0.0: react-scripts@5.0.0:
version "5.0.0" version "5.0.0"
@@ -9577,11 +9542,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url-loader@^4.0.0: resolve-url-loader@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57"
@@ -9903,15 +9863,15 @@ slash@^4.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
socket.io-client@^4.3.2: socket.io-client@4.4.0:
version "4.3.2" version "4.4.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.3.2.tgz#9cfdb8fecac8a24d5723daf8c8749e70c8fdeb25" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.4.0.tgz#d6568ebd79ac12e2d6b628e7e90e60f1d48d99ff"
integrity sha512-2B9LqSunN60yV8F7S84CCEEcgbYNfrn7ejIInZtLZ7ppWtiX8rGZAjvdCvbnC8bqo/9RlCNOUsORLyskxSFP1g== integrity sha512-g7riSEJXi7qCFImPow98oT8X++MSsHz6MMFRXkWNJ6uEROSHOa3kxdrsYWMq85dO+09CFMkcqlpjvbVXQl4z6g==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.0.0" "@socket.io/component-emitter" "~3.0.0"
backo2 "~1.0.2" backo2 "~1.0.2"
debug "~4.3.2" debug "~4.3.2"
engine.io-client "~6.0.1" engine.io-client "~6.1.1"
parseuri "0.0.6" parseuri "0.0.6"
socket.io-parser "~4.1.1" socket.io-parser "~4.1.1"
@@ -10219,10 +10179,10 @@ stylehacks@^5.0.1:
browserslist "^4.16.0" browserslist "^4.16.0"
postcss-selector-parser "^6.0.4" postcss-selector-parser "^6.0.4"
stylis@^4.0.10: stylis@4.0.13:
version "4.0.10" version "4.0.13"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg== integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@^4.0.3: stylis@^4.0.3:
version "4.0.9" version "4.0.9"
@@ -10409,16 +10369,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: tiny-invariant@^1.0.6:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.2.1: tmp@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -10729,11 +10684,6 @@ v8-to-istanbul@^8.1.0:
convert-source-map "^1.6.0" convert-source-map "^1.6.0"
source-map "^0.7.3" source-map "^0.7.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2: vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+16 -5
View File
@@ -118,10 +118,21 @@
"app": "ontime", "app": "ontime",
"version": 1, "version": 1,
"serverPort": 4001, "serverPort": 4001,
"oscInPort": 8888, "lock": null,
"oscOutPort": 9999, "pinCode": "1234"
"oscOutIP": "127.0.0.1", },
"oscEnabled": true, "osc": {
"lock": false "port": 8888,
"portOut": 9999,
"targetIP": "127.0.0.1",
"enabled": true
},
"aliases": [
{
"id": "0b0b3",
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
} }
]
} }
+15 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "0.4.8", "version": "0.5.0",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
@@ -13,17 +13,18 @@
"main": "main.js", "main": "main.js",
"devDependencies": { "devDependencies": {
"electron": "^13.6.1", "electron": "^13.6.1",
"electron-builder": "^22.11.3", "electron-builder": "^22.14.5",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"jest": "^27.0.4", "jest": "^27.4.5",
"prettier": "^2.5.1" "prettier": "^2.5.1"
}, },
"scripts": { "scripts": {
"nodestart": "NODE_ENV=development node src/app.js", "nodestart": "NODE_ENV=development node src/app.js",
"setdb": "cp data/db.json src/data/db.json", "setdb": "cp data/db.json src/data/db.json",
"clean": "rm -rf ../client/build/ && rm -rf ../client/node_modules && rm -rf src/node_modules && rm -rf ./node_modules && rm -rf ./dist", "clean": "rm -rf ../client/build/ && rm -rf ../client/node_modules && rm -rf src/node_modules && rm -rf ./node_modules && rm -rf ./dist",
"cheat": "rm src/node_modules/ontime-utils && cp -R utils src/node_modules/ontime-utils",
"prep": "yarn clean && yarn setdb", "prep": "yarn clean && yarn setdb",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"start": "NODE_ENV=development electron .", "start": "NODE_ENV=development electron .",
@@ -34,6 +35,8 @@
"dist-all": "electron-builder -mw" "dist-all": "electron-builder -mw"
}, },
"jest": { "jest": {
"testEnvironment": "node",
"testRunner": "jasmine2",
"testPathIgnorePatterns": [ "testPathIgnorePatterns": [
"dist" "dist"
] ]
@@ -94,6 +97,15 @@
"!**/{yarn.lock,yarn-error.log}", "!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}" "!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
] ]
},
{
"from": "utils",
"to": "extraResources/utils",
"filter": [
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
]
} }
] ]
}, },
+9 -7
View File
@@ -1,6 +1,7 @@
// get environment vars // get environment vars
import 'dotenv/config'; import 'dotenv/config';
import { sessionId, user } from './utils/analytics.js'; import { sessionId, user } from './utils/analytics.js';
user.screenview('Node service', 'ontime').send(); user.screenview('Node service', 'ontime').send();
user.event('NODE', 'started', 'starting node service').send(); user.event('NODE', 'started', 'starting node service').send();
@@ -12,6 +13,7 @@ import { Low, JSONFile } from 'lowdb';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -26,7 +28,7 @@ import express from 'express';
import http from 'http'; import http from 'http';
import cors from 'cors'; import cors from 'cors';
import { dbModelv1 as dbModel } from './models/dataModel.js'; import { dbModelv1 as dbModel } from './models/dataModel.js';
import { parseJsonv1 as parseJson, validateFile } from './utils/parser.js'; import { parseJson_v1 as parseJson, validateFile } from './utils/parser.js';
import ua from 'universal-analytics'; import ua from 'universal-analytics';
// validate JSON before attempting read // validate JSON before attempting read
@@ -86,8 +88,8 @@ app.use('/playback', playbackRouter);
// serve react // serve react
app.use( app.use(
express.static( express.static(
path.join(__dirname, env === 'prod' ? '../' : '../../', 'client/build') path.join(__dirname, env === 'prod' ? '../' : '../../', 'client/build'),
) ),
); );
app.get('*', (req, res) => { app.get('*', (req, res) => {
@@ -97,8 +99,8 @@ app.get('*', (req, res) => {
env === 'prod' ? '../' : '../../', env === 'prod' ? '../' : '../../',
'client', 'client',
'build', 'build',
'index.html' 'index.html',
) ),
); );
}); });
@@ -159,8 +161,8 @@ export const startServer = async (overrideConfig = null) => {
// OSC Config // OSC Config
const oscConfig = { const oscConfig = {
ip: oscIP, ip: oscIP,
port: overrideConfig?.port || oscOutPort port: overrideConfig?.port || oscOutPort,
} };
// init timer // init timer
global.timer = new EventTimer(server, config.timer, oscConfig, data.http); global.timer = new EventTimer(server, config.timer, oscConfig, data.http);
+9 -2
View File
@@ -10,8 +10,8 @@ import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js'; import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../utils/url.js'; import { cleanURL } from '../utils/url.js';
import getRandomName from '../utils/getRandomName.js'; import getRandomName from '../utils/getRandomName.js';
import { stringFromMillis } from '../utils/time.js'; import { stringFromMillis } from 'ontime-utils/time.js';
import { generateId } from '../utils/generate_id.js'; import { generateId } from 'ontime-utils/generate_id.js';
/* /*
* Class EventTimer adds functions specific to APP * Class EventTimer adds functions specific to APP
@@ -111,14 +111,21 @@ export class EventTimer extends Timer {
* @description Shutdown process * @description Shutdown process
*/ */
shutdown() { shutdown() {
clearInterval(this._interval);
this.info('SERVER', 'Shutting down ontime'); this.info('SERVER', 'Shutting down ontime');
if (this.io != null) {
this.info('TX', '... Closing socket server'); this.info('TX', '... Closing socket server');
this.io.close(); this.io.close();
}
if (this.osc != null) {
this.info('TX', '... Closing OSC Client'); this.info('TX', '... Closing OSC Client');
this.osc.shutdown(); this.osc.shutdown();
}
if (this.http != null) {
this.info('TX', '... Closing HTTP Client'); this.info('TX', '... Closing HTTP Client');
this.http.shutdown(); this.http.shutdown();
} }
}
/** /**
* Initialises OSC Integration object * Initialises OSC Integration object
+1 -1
View File
@@ -4,7 +4,7 @@
* *
*/ */
import { stringFromMillis } from '../utils/time.js'; import { stringFromMillis } from 'ontime-utils/time.js';
export class Timer { export class Timer {
constructor() { constructor() {
+20 -23
View File
@@ -1,24 +1,18 @@
import {EventTimer} from "../EventTimer"; import { EventTimer } from '../EventTimer';
import http from 'http'; import http from 'http';
import express from "express"; import express from 'express';
// Create server // Create server
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
// necessary config // necessary config
const timerConfig = {refresh: 1000}; const timerConfig = { refresh: 1000 };
beforeEach(async () => { afterAll(async () => {
server.listen(0, '0.0.0.0');
});
afterEach(async () => {
await server.close(); await server.close();
}); });
test('object instantiates correctly', async () => { test('object instantiates correctly', async () => {
const t = new EventTimer(server, timerConfig); const t = new EventTimer(server, timerConfig);
@@ -41,16 +35,15 @@ test('object instantiates correctly', async () => {
// and its own properties // and its own properties
expect(t.ontimeCycle).toBe('idle'); expect(t.ontimeCycle).toBe('idle');
expect(t.prevCycle).toBeNull(); expect(t.prevCycle).toBeNull();
expect(t.lastUpdate).toBeNull();
expect(t.io).not.toBeNull(); expect(t.io).not.toBeNull();
expect(t.osc).toBeNull(); expect(t.osc).toBeNull();
expect(t.http).toBeNull(); expect(t.http).toBeNull();
expect(t._numClients).toBe(0); expect(t._numClients).toBe(0);
expect(t._interval).not.toBeNull(); expect(t._interval).not.toBeNull();
expect(t.presenter).toStrictEqual({text: '', visible: false}); expect(t.presenter).toStrictEqual({ text: '', visible: false });
expect(t.public).toStrictEqual({text: '', visible: false}); expect(t.public).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({text: '', visible: false}); expect(t.lower).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({text: '', visible: false}); expect(t.lower).toStrictEqual({ text: '', visible: false });
const expectTitlesPublic = { const expectTitlesPublic = {
titleNow: null, titleNow: null,
@@ -78,19 +71,20 @@ test('object instantiates correctly', async () => {
expect(t.numEvents).toBe(0); expect(t.numEvents).toBe(0);
expect(t._eventlist).toBeNull(); expect(t._eventlist).toBeNull();
expect(t.onAir).toBeFalsy(); expect(t.onAir).toBeFalsy();
});
t.shutdown();
});
describe('test triggers behaviour', () => { describe('test triggers behaviour', () => {
const t = new EventTimer(server, timerConfig); const t = new EventTimer(server, timerConfig);
it('ignores bad commands', () => { test('ignores bad commands', async(done) => {
const success = t.trigger('test'); const success = t.trigger('test');
expect(success).toBeFalsy(); expect(success).toBeFalsy();
}) done();
});
it('does not allow triggering events with an empty list', () => {
test('does not allow triggering events with an empty list', async(done) => {
expect(t.numEvents).toBe(0); expect(t.numEvents).toBe(0);
expect(t.trigger('start')).toBeFalsy(); expect(t.trigger('start')).toBeFalsy();
@@ -106,10 +100,10 @@ describe('test triggers behaviour', () => {
expect(t.onAir).toBeTruthy(); expect(t.onAir).toBeTruthy();
expect(t.trigger('offAir')).toBeTruthy(); expect(t.trigger('offAir')).toBeTruthy();
expect(t.onAir).toBeFalsy(); expect(t.onAir).toBeFalsy();
done();
}); });
it('...and is consistent by calling the class methods', () => { test('...and is consistent by calling the class methods', async (done) => {
expect(t.numEvents).toBe(0); expect(t.numEvents).toBe(0);
expect(t.state).toBe('stop'); expect(t.state).toBe('stop');
@@ -133,5 +127,8 @@ describe('test triggers behaviour', () => {
t.reload(); t.reload();
expect(t.state).toBe('stop'); expect(t.state).toBe('stop');
done();
}); });
})
t.shutdown();
});
+1 -1
View File
@@ -2,7 +2,7 @@
import { db, data } from '../app.js'; import { db, data } from '../app.js';
// utils // utils
import { generateId } from '../utils/generate_id.js'; import { generateId } from 'ontime-utils/generate_id.js';
import { import {
event as eventDef, event as eventDef,
delay as delayDef, delay as delayDef,
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
import { db, data } from '../app.js'; import { db, data } from '../app.js';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js'; import { fileHandler } from '../utils/parser.js';
import { generateId } from 'ontime-utils/generate_id.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -116,6 +117,81 @@ export const getInfo = async (req, res) => {
}); });
}; };
// Create controller for POST request to '/ontime/aliases'
// Returns -
export const getAliases = async (req, res) => {
// send aliases array
res.status(200).send(data.aliases);
};
// Create controller for POST request to '/ontime/aliases'
// Returns ACK message
export const postAliases = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
const newAliases = [];
req.body.forEach((a) => {
newAliases.push({
id: generateId(),
enabled: a.enabled,
alias: a.alias,
pathAndParams: a.pathAndParams,
});
});
data.aliases = newAliases;
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/settings'
// Returns -
export const getSettings = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
// send object with network information
res.status(200).send({
version,
serverPort,
pinCode,
});
};
// Create controller for POST request to '/ontime/settings'
// Returns ACK message
export const postSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
try {
let pin = data.settings.pinCode;
if (typeof req.body?.pinCode === 'string') {
if (req.body?.pinCode.length === 0) {
pin = null;
} else if (req.body?.pinCode.length <= 4) {
pin = req.body?.pinCode;
}
}
data.settings = {
...data.settings,
pinCode: pin,
};
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/info' // Create controller for POST request to '/ontime/info'
// Returns ACK message // Returns ACK message
export const postInfo = async (req, res) => { export const postInfo = async (req, res) => {
+5
View File
@@ -0,0 +1,5 @@
export const alias = {
enabled: false,
alias: '',
pathAndParams: '',
};
+2
View File
@@ -12,7 +12,9 @@ export const dbModelv1 = {
version: 1, version: 1,
serverPort: 4001, serverPort: 4001,
lock: null, lock: null,
pinCode: null,
}, },
aliases: [],
osc: { osc: {
port: 8888, port: 8888,
portOut: 9999, portOut: 9999,
+5 -4
View File
@@ -7,13 +7,14 @@
"express": "~4.17.1", "express": "~4.17.1",
"express-session": "~1.17.1", "express-session": "~1.17.1",
"lowdb": "3.0.0", "lowdb": "3.0.0",
"multer": "^1.4.2", "multer": "^1.4.4",
"nanoid": "^3.1.22", "nanoid": "^3.1.30",
"node-osc": "6.1.10", "node-osc": "6.1.11",
"node-xlsx": "^0.17.2", "node-xlsx": "^0.17.2",
"ontime-utils": "link: ../server/utils/",
"passport": "~0.4.1", "passport": "~0.4.1",
"passport-local": "~1.0.0", "passport-local": "~1.0.0",
"socket.io": "^4.3.1", "socket.io": "4.4.0",
"universal-analytics": "^0.4.23" "universal-analytics": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
+17
View File
@@ -1,5 +1,6 @@
import express from 'express'; import express from 'express';
import { uploadFile } from '../utils/upload.js'; import { uploadFile } from '../utils/upload.js';
export const router = express.Router(); export const router = express.Router();
import { import {
@@ -10,6 +11,10 @@ import {
dbPathToUpload, dbPathToUpload,
getOSC, getOSC,
postOSC, postOSC,
getSettings,
postSettings,
getAliases,
postAliases,
} from '../controllers/ontimeController.js'; } from '../controllers/ontimeController.js';
// create route between controller and '/ontime/db' endpoint // create route between controller and '/ontime/db' endpoint
@@ -18,6 +23,18 @@ router.get('/db', dbDownload);
// create route between controller and '/ontime/db' endpoint // create route between controller and '/ontime/db' endpoint
router.post('/db', uploadFile, dbUpload); router.post('/db', uploadFile, dbUpload);
// create route between controller and '/ontime/settings' endpoint
router.get('/settings', getSettings);
// create route between controller and '/ontime/settings' endpoint
router.post('/settings', postSettings);
// create route between controller and '/ontime/aliases' endpoint
router.get('/aliases', getAliases);
// create route between controller and '/ontime/aliases' endpoint
router.post('/aliases', postAliases);
// create route between controller and '/ontime/info' endpoint // create route between controller and '/ontime/info' endpoint
router.get('/info', getInfo); router.get('/info', getInfo);
@@ -1,20 +0,0 @@
import { generateId } from '../generate_id.js';
describe('generate a valid id', () => {
it('generates a 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
});
describe('generate 100', () => {
it('all ids are unique', () => {
let ids = [];
for (let i = 0; i < 100; i++) {
ids.push(generateId());
}
const unique = [...new Set(ids)];
expect(ids.length).toBe(unique.length);
});
});
@@ -1,11 +1,11 @@
import getRandomName from '../getRandomName.js'; import getRandomName from '../getRandomName.js';
test('generates 500 unique names', () => { test('generates 100 unique names', () => {
let names = []; const names = new Set();
for (let i = 0; i < 500; i++) { let attempts = 1;
names.push(getRandomName()); while (names.size < 100) {
names.add(getRandomName());
attempts++;
} }
expect(attempts).toBeLessThan(105);
const unique = [...new Set(names)];
expect(names.length).toBe(unique.length);
}); });
+44 -19
View File
@@ -1,9 +1,10 @@
import jest from 'jest-mock'; import jest from 'jest-mock';
import { import {
makeString, makeString,
parseExcelv1, parseAliases_v1,
parseJsonv1, parseExcel_v1,
validateEventv1, parseJson_v1,
validateEvent_v1,
} from '../parser.js'; } from '../parser.js';
import { dbModelv1 as dbModel } from '../../models/dataModel.js'; import { dbModelv1 as dbModel } from '../../models/dataModel.js';
@@ -110,7 +111,7 @@ describe('test json parser with valid def', () => {
let parseResponse; let parseResponse;
beforeEach(async () => { beforeEach(async () => {
parseResponse = await parseJsonv1(testData); parseResponse = await parseJson_v1(testData);
}); });
it('has 7 events', () => { it('has 7 events', () => {
@@ -169,7 +170,7 @@ describe('test parser edge cases', () => {
], ],
}; };
const parseResponse = await parseJsonv1(testData); const parseResponse = await parseJson_v1(testData);
expect(parseResponse.events[0].id).toBeDefined(); expect(parseResponse.events[0].id).toBeDefined();
}); });
@@ -190,7 +191,7 @@ describe('test parser edge cases', () => {
], ],
}; };
const parseResponse = await parseJsonv1(testData); const parseResponse = await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith( expect(console.log).toHaveBeenCalledWith(
'ERROR: ID collision on import, skipping' 'ERROR: ID collision on import, skipping'
); );
@@ -212,7 +213,7 @@ describe('test parser edge cases', () => {
], ],
}; };
const parseResponse = await parseJsonv1(testData); const parseResponse = await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith( expect(console.log).toHaveBeenCalledWith(
'ERROR: undefined event type, skipping' 'ERROR: undefined event type, skipping'
); );
@@ -228,7 +229,7 @@ describe('test parser edge cases', () => {
}, },
}; };
await parseJsonv1(testData); await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith( expect(console.log).toHaveBeenCalledWith(
'ERROR: unknown app version, skipping' 'ERROR: unknown app version, skipping'
); );
@@ -271,7 +272,7 @@ describe('test corrupt data', () => {
}, },
}; };
const parsedDef = await parseJsonv1(emptyEvents); const parsedDef = await parseJson_v1(emptyEvents);
expect(parsedDef.events.length).toBe(2); expect(parsedDef.events.length).toBe(2);
}); });
@@ -293,7 +294,7 @@ describe('test corrupt data', () => {
}, },
}; };
const parsedDef = await parseJsonv1(emptyEvents); const parsedDef = await parseJson_v1(emptyEvents);
expect(parsedDef.events.length).toBe(0); expect(parsedDef.events.length).toBe(0);
}); });
@@ -309,7 +310,7 @@ describe('test corrupt data', () => {
}, },
}; };
const parsedDef = await parseJsonv1(emptyEventData); const parsedDef = await parseJson_v1(emptyEventData);
expect(parsedDef.event).toStrictEqual(dbModel.event); expect(parsedDef.event).toStrictEqual(dbModel.event);
}); });
@@ -323,14 +324,14 @@ describe('test corrupt data', () => {
}, },
}; };
const parsedDef = await parseJsonv1(missingSettings); const parsedDef = await parseJson_v1(missingSettings);
expect(parsedDef.settings).toStrictEqual(dbModel.settings); expect(parsedDef.settings).toStrictEqual(dbModel.settings);
}); });
it('fails with invalid JSON', async () => { it('fails with invalid JSON', async () => {
console.log = jest.fn(); console.log = jest.fn();
const invalidJSON = 'some random dataset'; const invalidJSON = 'some random dataset';
const parsedDef = await parseJsonv1(invalidJSON); const parsedDef = await parseJson_v1(invalidJSON);
expect(console.log).toHaveBeenCalledWith('ERROR: Invalid JSON format'); expect(console.log).toHaveBeenCalledWith('ERROR: Invalid JSON format');
expect(parsedDef).toBe(-1); expect(parsedDef).toBe(-1);
}); });
@@ -341,7 +342,7 @@ describe('test event validator', () => {
const event = { const event = {
title: 'test', title: 'test',
}; };
const validated = validateEventv1(event); const validated = validateEvent_v1(event);
expect(validated).toEqual( expect(validated).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -361,7 +362,7 @@ describe('test event validator', () => {
it('fails an empty object', () => { it('fails an empty object', () => {
const event = {}; const event = {};
const validated = validateEventv1(event); const validated = validateEvent_v1(event);
expect(validated).toEqual(null); expect(validated).toEqual(null);
}); });
@@ -372,7 +373,7 @@ describe('test event validator', () => {
presenter: 3.2, presenter: 3.2,
note: '1899-12-30T08:00:10.000Z', note: '1899-12-30T08:00:10.000Z',
}; };
const validated = validateEventv1(event); const validated = validateEvent_v1(event);
expect(typeof validated.title).toEqual('string'); expect(typeof validated.title).toEqual('string');
expect(typeof validated.subtitle).toEqual('string'); expect(typeof validated.subtitle).toEqual('string');
expect(typeof validated.presenter).toEqual('string'); expect(typeof validated.presenter).toEqual('string');
@@ -384,7 +385,7 @@ describe('test event validator', () => {
timeStart: false, timeStart: false,
timeEnd: '2', timeEnd: '2',
}; };
const validated = validateEventv1(event); const validated = validateEvent_v1(event);
expect(typeof validated.timeStart).toEqual('number'); expect(typeof validated.timeStart).toEqual('number');
expect(validated.timeStart).toEqual(0); expect(validated.timeStart).toEqual(0);
expect(typeof validated.timeEnd).toEqual('number'); expect(typeof validated.timeEnd).toEqual('number');
@@ -395,7 +396,7 @@ describe('test event validator', () => {
const event = { const event = {
title: {}, title: {},
}; };
const validated = validateEventv1(event); const validated = validateEvent_v1(event);
expect(typeof validated.title).toEqual('string'); expect(typeof validated.title).toEqual('string');
}); });
}); });
@@ -485,10 +486,34 @@ describe('test parseExcel function', () => {
}, },
]; ];
const parsedData = await parseExcelv1(testdata); const parsedData = await parseExcel_v1(testdata);
expect(parsedData.events).toBeDefined(); expect(parsedData.events).toBeDefined();
expect(parsedData.events).toStrictEqual(expectedParsedEvents); expect(parsedData.events).toStrictEqual(expectedParsedEvents);
expect(parsedData.events).toStrictEqual(expectedParsedEvents); expect(parsedData.events).toStrictEqual(expectedParsedEvents);
}); });
}); });
describe('test aliases import', () => {
it('imports a well defined alias', () => {});
const testData = {
events: [],
settings: {
app: 'ontime',
version: 1,
},
aliases: [
{
enabled: false,
alias: 'testalias',
pathAndParams: 'testpathAndParams',
},
],
};
const parsed = parseAliases_v1(testData, true);
expect(parsed.length).toBe(1);
// generates missing id
expect(parsed[0].id).toBeDefined();
});
+210 -128
View File
@@ -6,8 +6,8 @@ import {
block as blockDef, block as blockDef,
} from '../models/eventsDefinition.js'; } from '../models/eventsDefinition.js';
import { dbModelv1 } from '../models/dataModel.js'; import { dbModelv1 } from '../models/dataModel.js';
import { generateId } from './generate_id.js'; import { generateId } from 'ontime-utils/generate_id.js';
import { excelDateStringToMillis } from './time.js'; import { excelDateStringToMillis } from 'ontime-utils/time.js';
export const EXCEL_MIME = export const EXCEL_MIME =
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
@@ -36,8 +36,8 @@ export const fileHandler = async (file) => {
// we only look at worksheets called ontime or event schedule // we only look at worksheets called ontime or event schedule
if (excelData?.data) { if (excelData?.data) {
const dataFromExcel = await parseExcelv1(excelData.data); const dataFromExcel = await parseExcel_v1(excelData.data);
res.data = await parseJsonv1(dataFromExcel); res.data = await parseJson_v1(dataFromExcel);
res.message = 'success'; res.message = 'success';
} else { } else {
console.log('Error: No sheets found named ontime or event schedule'); console.log('Error: No sheets found named ontime or event schedule');
@@ -64,7 +64,7 @@ export const fileHandler = async (file) => {
if (uploadedJson.settings.version === 1) { if (uploadedJson.settings.version === 1) {
try { try {
res.data = await parseJsonv1(uploadedJson); res.data = await parseJson_v1(uploadedJson);
res.message = 'success'; res.message = 'success';
} catch (error) { } catch (error) {
res = { error: true, message: `Error parsing file: ${error}` }; res = { error: true, message: `Error parsing file: ${error}` };
@@ -75,7 +75,7 @@ export const fileHandler = async (file) => {
} }
// delete file // delete file
deleteFile(file); await deleteFile(file);
return res; return res;
}; };
@@ -85,7 +85,7 @@ export const fileHandler = async (file) => {
* @param {array} excelData - array with excel sheet * @param {array} excelData - array with excel sheet
* @returns {object} - parsed object * @returns {object} - parsed object
*/ */
export const parseExcelv1 = async (excelData) => { export const parseExcel_v1 = async (excelData) => {
let eventData = { let eventData = {
title: '', title: '',
url: '', url: '',
@@ -200,136 +200,27 @@ export const parseExcelv1 = async (excelData) => {
* @param {boolean} [enforce=false] - flag, tells to create an object anyway * @param {boolean} [enforce=false] - flag, tells to create an object anyway
* @returns {object} - parsed object * @returns {object} - parsed object
*/ */
export const parseJson_v1 = async (jsonData, enforce = false) => {
export const parseJsonv1 = async (jsonData, enforce=false) => {
if (!jsonData || typeof jsonData !== 'object') { if (!jsonData || typeof jsonData !== 'object') {
console.log('ERROR: Invalid JSON format'); console.log('ERROR: Invalid JSON format');
return -1; return -1;
} }
let numEntries = 0; // object containing the parsed data
let returnData = {}; let returnData = {};
if ('events' in jsonData) {
console.log('Found events definition, importing...');
let events = [];
let ids = [];
for (const e of jsonData.events) {
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
let event = validateEventv1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
numEntries++;
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
numEntries++;
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
numEntries++;
} else {
console.log('ERROR: undefined event type, skipping');
}
}
// write to db
returnData.events = events;
console.log(`Uploaded file with ${numEntries} entries`);
} else if (enforce) {
returnData.events = [];
console.log(`Created events object in db`);
}
if ('event' in jsonData) {
console.log('Found event data, importing...');
const e = jsonData.event;
// filter known properties and write to db
returnData.event = {
...dbModelv1.event,
title: e.title || dbModelv1.event.title,
url: e.url || dbModelv1.event.url,
publicInfo: e.publicInfo || dbModelv1.event.publicInfo,
backstageInfo: e.backstageInfo || dbModelv1.event.backstageInfo,
endMessage: e.endMessage || dbModelv1.event.endMessage,
};
} else if (enforce) {
returnData.event = dbModelv1.event;
console.log(`Created event object in db`);
}
// parse Events
returnData.events = parseEvents_v1(jsonData);
// parse Event
returnData.event = parseEvent_v1(jsonData, enforce);
// Settings handled partially // Settings handled partially
if ('settings' in jsonData) { returnData.settings = parseSettings_v1(jsonData, enforce);
console.log('Found settings definition, importing...');
const s = jsonData.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
let settings = {
lock: s.lock || null,
};
// write to db
returnData.settings = {
...dbModelv1.settings,
...settings,
};
}
} else if (enforce) {
returnData.settings = dbModelv1.settings;
console.log(`Created settings object in db`);
}
// Import OSC settings if any // Import OSC settings if any
if ('osc' in jsonData) { returnData.osc = parseOsc_v1(jsonData, enforce);
console.log('Found OSC definition, importing...');
const s = jsonData.osc;
let osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (s.enabled) osc.enabled = s.enabled;
// write to db
returnData.osc = {
...dbModelv1.osc,
...osc,
};
} else if (enforce) {
returnData.osc = dbModelv1.osc;
console.log(`Created osc object in db`);
}
// Import HTTP settings if any // Import HTTP settings if any
if ('http' in jsonData) { returnData.http = parseHttp_v1(jsonData, enforce);
console.log('Found HTTP definition, importing...'); // Import Aliases if any
const h = jsonData.osc; returnData.aliases = parseAliases_v1(jsonData);
let http = {};
if (h.user) http.user = h.user;
if (h.pwd) http.pwd = h.pwd;
// write to db
returnData.http = {
...dbModelv1.http,
...http,
};
} else if (enforce) {
returnData.http = dbModelv1.http;
console.log(`Created http object in db`);
}
return returnData; return returnData;
}; };
@@ -352,7 +243,7 @@ export const makeString = (val, fallback = '') => {
* @returns {object|null} - formatted object or null in case is invalid * @returns {object|null} - formatted object or null in case is invalid
*/ */
export const validateEventv1 = (eventArgs) => { export const validateEvent_v1 = (eventArgs) => {
// ensure id is defined and unique // ensure id is defined and unique
const id = eventArgs.id || generateId(); const id = eventArgs.id || generateId();
let event = null; let event = null;
@@ -418,3 +309,194 @@ export const validateFile = (file) => {
return false; return false;
} }
}; };
/**
* Parse events array of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseEvents_v1 = (data) => {
let newEvents = [];
if ('events' in data) {
console.log('Found events definition, importing...');
let events = [];
let ids = [];
for (const e of data.events) {
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
let event = validateEvent_v1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
}
}
// write to db
newEvents = events;
console.log(`Uploaded file with ${events.length} entries`);
}
return newEvents;
};
/**
* Parse event 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 parseEvent_v1 = (data, enforce) => {
let newEvent = {};
if ('event' in data) {
console.log('Found event data, importing...');
const e = data.event;
// filter known properties and write to db
newEvent = {
...dbModelv1.event,
title: e.title || dbModelv1.event.title,
url: e.url || dbModelv1.event.url,
publicInfo: e.publicInfo || dbModelv1.event.publicInfo,
backstageInfo: e.backstageInfo || dbModelv1.event.backstageInfo,
endMessage: e.endMessage || dbModelv1.event.endMessage,
};
} else if (enforce) {
newEvent = dbModelv1.event;
console.log(`Created event object in db`);
}
return newEvent;
};
/**
* Parse settings 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 parseSettings_v1 = (data, enforce) => {
let newSettings = {};
if ('settings' in data) {
console.log('Found settings definition, importing...');
const s = data.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
let settings = {
lock: s.lock || null,
pinCode: s.pinCode || null,
};
// write to db
newSettings = {
...dbModelv1.settings,
...settings,
};
}
} else if (enforce) {
newSettings = dbModelv1.settings;
console.log(`Created settings object in db`);
}
return newSettings;
};
/**
* Parse osc 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 parseOsc_v1 = (data, enforce) => {
let newOsc = {};
if ('osc' in data) {
console.log('Found OSC definition, importing...');
const s = data.osc;
let osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (s.enabled) osc.enabled = s.enabled;
// write to db
newOsc = {
...dbModelv1.osc,
...osc,
};
} else if (enforce) {
newOsc = dbModelv1.osc;
console.log(`Created OSC object in db`);
}
return newOsc;
};
/**
* 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_v1 = (data, enforce) => {
let newHttp = {};
if ('http' in data) {
console.log('Found HTTP definition, importing...');
const h = data.osc;
let http = {};
if (h.user) http.user = h.user;
if (h.pwd) http.pwd = h.pwd;
// write to db
newHttp.http = {
...dbModelv1.http,
...http,
};
} else if (enforce) {
newHttp.http = dbModelv1.http;
console.log(`Created http object in db`);
}
return newHttp;
};
/**
* Parse aliases portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseAliases_v1 = (data) => {
let newAliases = [];
if ('aliases' in data) {
console.log('Found Aliases definition, importing...');
const ids = [];
for (const a of data.aliases) {
// double check unique ids
if (ids.indexOf(a?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
const newAlias = {
id: a.id || generateId(),
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
};
ids.push(newAlias.id);
newAliases.push(newAlias);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
}
return newAliases;
};
+27 -23
View File
@@ -460,10 +460,10 @@ engine.io-parser@~5.0.0:
dependencies: dependencies:
base64-arraybuffer "~1.0.1" base64-arraybuffer "~1.0.1"
engine.io@~6.0.0: engine.io@~6.1.0:
version "6.0.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0" resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.0.tgz#459eab0c3724899d7b63a20c3a6835cf92857939"
integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg== integrity sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==
dependencies: dependencies:
"@types/cookie" "^0.4.1" "@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12" "@types/cors" "^2.8.12"
@@ -1059,10 +1059,10 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
multer@^1.4.2: multer@^1.4.4:
version "1.4.3" version "1.4.4"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b" resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg== integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
dependencies: dependencies:
append-field "^1.0.0" append-field "^1.0.0"
busboy "^0.2.11" busboy "^0.2.11"
@@ -1073,7 +1073,7 @@ multer@^1.4.2:
type-is "^1.6.4" type-is "^1.6.4"
xtend "^4.0.0" xtend "^4.0.0"
nanoid@^3.1.22: nanoid@^3.1.30:
version "3.1.30" version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
@@ -1088,10 +1088,10 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
node-osc@6.1.10: node-osc@6.1.11:
version "6.1.10" version "6.1.11"
resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.1.10.tgz#f618281f1f4fe91a41bc1414f58322369011e1ed" resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.1.11.tgz#c1d0700cb74e7fe141f4f99531318b8615e38d2f"
integrity sha512-HiDNie6meSzfUcmso8jGyeQ1TTWC2G9FI/vO57H5JaNGAUCCh9X8wDzTZy0KpDTSBxxNC2Rgr8kCC7f0oJ/gww== integrity sha512-bwFV/UKBBaSNK5pLYeA8Qx0GVc49fKRjmfMwMoypfPwb7RdhyRccW+tTNHcwr/OJ2b01uVKpnTPurSNs4fWnvw==
dependencies: dependencies:
osc-min "^1.1.1" osc-min "^1.1.1"
@@ -1133,6 +1133,10 @@ once@^1.3.0:
dependencies: dependencies:
wrappy "1" wrappy "1"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
optionator@^0.9.1: optionator@^0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@@ -1426,10 +1430,10 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
socket.io-adapter@~2.3.2: socket.io-adapter@~2.3.3:
version "2.3.2" version "2.3.3"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486"
integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg== integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==
socket.io-parser@~4.0.4: socket.io-parser@~4.0.4:
version "4.0.4" version "4.0.4"
@@ -1440,16 +1444,16 @@ socket.io-parser@~4.0.4:
component-emitter "~1.3.0" component-emitter "~1.3.0"
debug "~4.3.1" debug "~4.3.1"
socket.io@^4.3.1: socket.io@4.4.0:
version "4.3.1" version "4.4.0"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763" resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.0.tgz#8140a0db2c22235f88a6dceb867e4d5c9bd70507"
integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA== integrity sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==
dependencies: dependencies:
accepts "~1.3.4" accepts "~1.3.4"
base64id "~2.0.0" base64id "~2.0.0"
debug "~4.3.2" debug "~4.3.2"
engine.io "~6.0.0" engine.io "~6.1.0"
socket.io-adapter "~2.3.2" socket.io-adapter "~2.3.3"
socket.io-parser "~4.0.4" socket.io-parser "~4.0.4"
ssf@~0.11.2: ssf@~0.11.2:
@@ -0,0 +1,28 @@
import { generateId } from '../generate_id.js';
test('generate a valid 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
test('generate 100 with less than 110 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 100) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(105);
});
describe('generate 1000 with less than 1020 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 1000) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(1020);
});
@@ -1,6 +1,6 @@
import { excelDateStringToMillis, stringFromMillis } from '../time.js'; import { excelDateStringToMillis, stringFromMillis } from '../time.js';
describe('test string to milis function', () => { describe('test string to millis function', () => {
it('test with null values', () => { it('test with null values', () => {
const t = { val: null, result: '...' }; const t = { val: null, result: '...' };
expect(stringFromMillis(t.val)).toBe(t.result); expect(stringFromMillis(t.val)).toBe(t.result);
@@ -74,6 +74,6 @@ describe('test excel date parser', () => {
it('handles an invalid date string', () => { it('handles an invalid date string', () => {
const s = 'hello'; const s = 'hello';
expect(excelDateStringToMillis(s)).toBe(null); expect(excelDateStringToMillis(s)).toBe(0);
}); });
}); });
+11
View File
@@ -0,0 +1,11 @@
{
"name": "ontime-utils",
"type": "module",
"private": true,
"version": "1.0.0",
"description": "ontime app utility functions",
"dependencies": {
"nanoid": "^3.1.30"
},
"devDependencies": {}
}
+67
View File
@@ -0,0 +1,67 @@
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
/**
* Returns current time in milliseconds
* @returns {number}
*/
export const nowInMillis = () => {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
};
/**
* @description Converts milliseconds to string representing time
* @param {number} ms - time in milliseconds
* @param {boolean} showSeconds - weather to show the seconds
* @param {string} delim - character between HH MM SS
* @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02
*/
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms);
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
const minutes = showWith0(Math.floor((millis / mtm) % 60));
const seconds = showWith0(Math.floor((millis / mts) % 60));
return showSeconds
? `${isNegative}${
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
}${minutes}${delim}${seconds}`
: `${isNegative}${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
};
/**
* @description Converts an excel date to milliseconds
* @argument {string} excelDate - excel string date
* @returns {number} - time in milliseconds
*/
export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
const h = date.getUTCHours();
const m = date.getMinutes();
const s = date.getSeconds();
return h * mth + m * mtm + s * mts;
}
return 0;
};
+8
View File
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+2 -2
View File
@@ -1562,7 +1562,7 @@ ejs@^3.1.6:
dependencies: dependencies:
jake "^10.6.1" jake "^10.6.1"
electron-builder@^22.11.3: electron-builder@^22.14.5:
version "22.14.5" version "22.14.5"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-22.14.5.tgz#3a25547bd4fe3728d4704da80956a794c5c31496" resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-22.14.5.tgz#3a25547bd4fe3728d4704da80956a794c5c31496"
integrity sha512-N73hSbXFz6Mz5Z6h6C5ly6CB+dUN6k1LuCDJjI8VF47bMXv/QE0HE+Kkb0GPKqTqM7Hsk/yIYX+kHCfSkR5FGg== integrity sha512-N73hSbXFz6Mz5Z6h6C5ly6CB+dUN6k1LuCDJjI8VF47bMXv/QE0HE+Kkb0GPKqTqM7Hsk/yIYX+kHCfSkR5FGg==
@@ -2857,7 +2857,7 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0" merge-stream "^2.0.0"
supports-color "^8.0.0" supports-color "^8.0.0"
jest@^27.0.4: jest@^27.4.5:
version "27.4.5" version "27.4.5"
resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.5.tgz#66e45acba44137fac26be9d3cc5bb031e136dc0f" resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.5.tgz#66e45acba44137fac26be9d3cc5bb031e136dc0f"
integrity sha512-uT5MiVN3Jppt314kidCk47MYIRilJjA/l2mxwiuzzxGUeJIvA8/pDaJOAX5KWvjAo7SCydcW0/4WEtgbLMiJkg== integrity sha512-uT5MiVN3Jppt314kidCk47MYIRilJjA/l2mxwiuzzxGUeJIvA8/pDaJOAX5KWvjAo7SCydcW0/4WEtgbLMiJkg==