mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
v2 beta6 (#379)
* feat: operator key * chore: update demo * chore: version bump * fix: prevent stale keys * refactor: add new fields to validation * style: tweaks to modal arrangement * refactor: prevent parsing http * ux: click anywhere in event to edit * refactor: performance improvements to time input * refactor: performance improvements menu
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.0.0-beta5",
|
"version": "2.0.0-beta",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.5.5",
|
"@chakra-ui/react": "^2.5.5",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const SPublic = withData(Public);
|
|||||||
const SLowerThird = withData(Lower);
|
const SLowerThird = withData(Lower);
|
||||||
const SStudio = withData(StudioClock);
|
const SStudio = withData(StudioClock);
|
||||||
|
|
||||||
const FeatureWrapper = lazy(() => import('./features/FeatureWrapper'));
|
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
|
||||||
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
||||||
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
||||||
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
||||||
@@ -83,33 +83,33 @@ export default function AppRouter() {
|
|||||||
<Route
|
<Route
|
||||||
path='/rundown'
|
path='/rundown'
|
||||||
element={
|
element={
|
||||||
<FeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<RundownPanel />
|
<RundownPanel />
|
||||||
</FeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/timercontrol'
|
path='/timercontrol'
|
||||||
element={
|
element={
|
||||||
<FeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<TimerControl />
|
<TimerControl />
|
||||||
</FeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/messagecontrol'
|
path='/messagecontrol'
|
||||||
element={
|
element={
|
||||||
<FeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<MessageControl />
|
<MessageControl />
|
||||||
</FeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/info'
|
path='/info'
|
||||||
element={
|
element={
|
||||||
<FeatureWrapper>
|
<EditorFeatureWrapper>
|
||||||
<Info />
|
<Info />
|
||||||
</FeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/*/!* Send to default if nothing found *!/*/}
|
{/*/!* Send to default if nothing found *!/*/}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -21,6 +21,20 @@ interface TimeInputProps {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ButtonInitial(name: TimeEntryField) {
|
||||||
|
if (name === 'timeStart') return 'S';
|
||||||
|
if (name === 'timeEnd') return 'E';
|
||||||
|
if (name === 'durationOverride') return 'D';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonTooltip(name: TimeEntryField, warning?: string) {
|
||||||
|
if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`;
|
||||||
|
if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`;
|
||||||
|
if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export default function TimeInput(props: TimeInputProps) {
|
export default function TimeInput(props: TimeInputProps) {
|
||||||
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
|
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
@@ -142,30 +156,24 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (time == null) return;
|
if (time == null) return;
|
||||||
resetValue();
|
resetValue();
|
||||||
}, [emitError, resetValue, time]);
|
}, [resetValue, time]);
|
||||||
|
|
||||||
const ButtonInitial = () => {
|
|
||||||
if (name === 'timeStart') return 'S';
|
|
||||||
if (name === 'timeEnd') return 'E';
|
|
||||||
if (name === 'durationOverride') return 'D';
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const ButtonTooltip = () => {
|
|
||||||
if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`;
|
|
||||||
if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`;
|
|
||||||
if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`;
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDelayed = delay !== 0;
|
const isDelayed = delay !== 0;
|
||||||
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
|
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
|
||||||
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
|
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
|
||||||
|
|
||||||
|
const TooltipLabel = useMemo(() => {
|
||||||
|
return ButtonTooltip(name, warning);
|
||||||
|
}, [name, warning]);
|
||||||
|
|
||||||
|
const ButtonText = useMemo(() => {
|
||||||
|
return ButtonInitial(name);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup size='sm' className={inputClasses}>
|
<InputGroup size='sm' className={inputClasses}>
|
||||||
<InputLeftElement className={style.inputLeft}>
|
<InputLeftElement className={style.inputLeft}>
|
||||||
<Tooltip label={ButtonTooltip()} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
@@ -175,7 +183,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
borderRight='1px solid transparent'
|
borderRight='1px solid transparent'
|
||||||
borderRadius='2px 0 0 2px'
|
borderRadius='2px 0 0 2px'
|
||||||
>
|
>
|
||||||
{ButtonInitial()}
|
{ButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</InputLeftElement>
|
</InputLeftElement>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
||||||
|
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||||
|
|
||||||
|
import style from './ProtectRoute.module.scss';
|
||||||
|
|
||||||
|
interface PinPageProps {
|
||||||
|
permission: 'editor' | 'operator';
|
||||||
|
handleValidation: (pin: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PinPage(props: PinPageProps) {
|
||||||
|
const { permission, handleValidation } = props;
|
||||||
|
const [pin, setPin] = useState('');
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
|
const validate = useCallback(() => {
|
||||||
|
const isValid = handleValidation(pin);
|
||||||
|
if (!isValid) {
|
||||||
|
setFailed(true);
|
||||||
|
setPin('');
|
||||||
|
}
|
||||||
|
}, [handleValidation, pin]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
|
if (event.repeat) return;
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
validate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
};
|
||||||
|
}, [validate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.container}>
|
||||||
|
{`Ontime ${permission || ''}`}
|
||||||
|
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
||||||
|
<PinInput
|
||||||
|
type='alphanumeric'
|
||||||
|
size='lg'
|
||||||
|
mask
|
||||||
|
autoFocus
|
||||||
|
value={pin}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFailed(false);
|
||||||
|
setPin(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PinInputField />
|
||||||
|
<PinInputField />
|
||||||
|
<PinInputField />
|
||||||
|
<PinInputField />
|
||||||
|
</PinInput>
|
||||||
|
<IconButton aria-label='Enter' size='lg' isRound icon={<IoCheckmark />} onClick={validate} />
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
|
||||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { AppContext } from '../../context/AppContext';
|
|
||||||
|
|
||||||
import style from './ProtectRoute.module.scss';
|
|
||||||
|
|
||||||
export default function ProtectRoute({ children }) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
const handleValidation = useCallback(() => {
|
|
||||||
const r = validate(pin);
|
|
||||||
if (!r) {
|
|
||||||
setFailed(true);
|
|
||||||
setPin('');
|
|
||||||
}
|
|
||||||
}, [pin, validate]);
|
|
||||||
|
|
||||||
// Set window title
|
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'ontime';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(e) => {
|
|
||||||
// handle held key
|
|
||||||
if (e.repeat) return;
|
|
||||||
// Space bar
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
handleValidation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleValidation]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// attach the event listener
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
|
||||||
|
|
||||||
// remove the event listener
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyPress);
|
|
||||||
};
|
|
||||||
}, [handleKeyPress]);
|
|
||||||
|
|
||||||
if (isLocal || auth) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.container}>
|
|
||||||
ontime
|
|
||||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
|
||||||
<PinInput
|
|
||||||
type='alphanumeric'
|
|
||||||
size='lg'
|
|
||||||
mask
|
|
||||||
autoFocus
|
|
||||||
value={pin}
|
|
||||||
onChange={(value) => {
|
|
||||||
setFailed(false);
|
|
||||||
setPin(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
</PinInput>
|
|
||||||
<IconButton
|
|
||||||
aria-label='Enter'
|
|
||||||
size='lg'
|
|
||||||
isRound
|
|
||||||
icon={<FiCheck />}
|
|
||||||
onClick={() => handleValidation()}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtectRoute.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { PropsWithChildren, useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import { AppContext } from '../../context/AppContext';
|
||||||
|
|
||||||
|
import PinPage from './PinPage';
|
||||||
|
|
||||||
|
interface ProtectRouteProps {
|
||||||
|
permission: 'editor' | 'operator';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProtectRoute({ permission, children }: PropsWithChildren<ProtectRouteProps>) {
|
||||||
|
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||||
|
const { editorAuth, operatorAuth, validate } = useContext(AppContext);
|
||||||
|
|
||||||
|
const handleValidation = useCallback(
|
||||||
|
(pin: string) => {
|
||||||
|
return validate(pin, permission);
|
||||||
|
},
|
||||||
|
[permission, validate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRelevantAuth = () => {
|
||||||
|
if (permission === 'editor') {
|
||||||
|
return editorAuth;
|
||||||
|
}
|
||||||
|
if (permission === 'operator') {
|
||||||
|
return operatorAuth;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLocal || hasRelevantAuth()) {
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment -- trying to make typescript happy
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PinPage permission={permission} handleValidation={handleValidation} />;
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import useSettings from '../hooks-query/useSettings';
|
|
||||||
|
|
||||||
export const AppContext = createContext({
|
|
||||||
auth: false,
|
|
||||||
data: {
|
|
||||||
pinCode: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AppContextProvider = ({ children }) => {
|
|
||||||
const [auth, setAuth] = useState(true);
|
|
||||||
const { data } = useSettings();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data == null) return;
|
|
||||||
const previousEntry = sessionStorage.getItem('ontime-entry');
|
|
||||||
if (previousEntry) {
|
|
||||||
if (previousEntry === data?.pinCode) {
|
|
||||||
setAuth(true);
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem('ontime-entry');
|
|
||||||
}
|
|
||||||
} else if (data?.pinCode == null || data?.pinCode === '') {
|
|
||||||
setAuth(true);
|
|
||||||
} else {
|
|
||||||
setAuth(false);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a pincode
|
|
||||||
* @return boolean - whether the pin is valid
|
|
||||||
*/
|
|
||||||
const validate = useCallback(
|
|
||||||
(pin) => {
|
|
||||||
let correct;
|
|
||||||
if (data?.pinCode == null || data?.pinCode === '') {
|
|
||||||
correct = true;
|
|
||||||
} else {
|
|
||||||
correct = pin === data?.pinCode;
|
|
||||||
}
|
|
||||||
if (correct) {
|
|
||||||
sessionStorage.setItem('ontime-entry', pin);
|
|
||||||
}
|
|
||||||
setAuth(correct);
|
|
||||||
return correct;
|
|
||||||
},
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createContext, PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import useSettings from '../hooks-query/useSettings';
|
||||||
|
|
||||||
|
interface AppContextType {
|
||||||
|
editorAuth: boolean;
|
||||||
|
operatorAuth: boolean;
|
||||||
|
validate: (pin: string, permission: 'editor' | 'operator') => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppContext = createContext<AppContextType>({
|
||||||
|
editorAuth: false,
|
||||||
|
operatorAuth: false,
|
||||||
|
validate: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageKeys = {
|
||||||
|
editor: 'ontime-editor-entry',
|
||||||
|
operator: 'ontime-operator-entry',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const { status, data } = useSettings();
|
||||||
|
const [editorAuth, setEditorAuth] = useState(true);
|
||||||
|
const [operatorAuth, setOperatorAuth] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'loading') return;
|
||||||
|
if (!data) return;
|
||||||
|
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
||||||
|
|
||||||
|
if (previousEditor && previousEditor === data.editorKey) {
|
||||||
|
setEditorAuth(true);
|
||||||
|
} else {
|
||||||
|
setEditorAuth(data.editorKey == null || data.editorKey === '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousOperator = sessionStorage.getItem(storageKeys.operator);
|
||||||
|
if (previousOperator && previousOperator === data.operatorKey) {
|
||||||
|
setOperatorAuth(true);
|
||||||
|
} else {
|
||||||
|
setOperatorAuth(data.operatorKey == null || data.operatorKey === '');
|
||||||
|
}
|
||||||
|
}, [data, status]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a pincode
|
||||||
|
* @return boolean - whether the pin is valid
|
||||||
|
*/
|
||||||
|
const validate = useCallback(
|
||||||
|
(pin: string, permission: 'editor' | 'operator'): boolean => {
|
||||||
|
function isValid(pin: string, savedPin?: string | null): boolean {
|
||||||
|
return savedPin == null || savedPin === '' || pin === savedPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === 'editor') {
|
||||||
|
const correct = isValid(pin, data.editorKey);
|
||||||
|
if (correct) {
|
||||||
|
sessionStorage.setItem(storageKeys.editor, pin);
|
||||||
|
}
|
||||||
|
setEditorAuth(correct);
|
||||||
|
return correct;
|
||||||
|
} else if (permission === 'operator') {
|
||||||
|
const correct = isValid(pin, data.operatorKey);
|
||||||
|
if (correct) {
|
||||||
|
sessionStorage.setItem(storageKeys.operator, pin);
|
||||||
|
}
|
||||||
|
setOperatorAuth(correct);
|
||||||
|
return correct;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AppContext.Provider value={{ editorAuth, operatorAuth, validate }}>{children}</AppContext.Provider>;
|
||||||
|
};
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
export const ontimeQueryClient = new QueryClient();
|
export const ontimeQueryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
cacheTime: 1000 * 60 * 10, // 10 min
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
+3
-7
@@ -1,16 +1,12 @@
|
|||||||
import { ReactNode } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import ProtectRoute from '../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../common/components/protect-route/ProtectRoute';
|
||||||
|
|
||||||
import style from './FeatureWrapper.module.scss';
|
import style from './FeatureWrapper.module.scss';
|
||||||
|
|
||||||
interface FeatureWrapperProps {
|
export default function EditorFeatureWrapper({ children }: PropsWithChildren) {
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FeatureWrapper({ children }: FeatureWrapperProps) {
|
|
||||||
return (
|
return (
|
||||||
<ProtectRoute>
|
<ProtectRoute permission='editor'>
|
||||||
<div className={style.wrapper}>{children}</div>
|
<div className={style.wrapper}>{children}</div>
|
||||||
</ProtectRoute>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import Editor from './Editor';
|
|||||||
|
|
||||||
export default function ProtectedEditor() {
|
export default function ProtectedEditor() {
|
||||||
return (
|
return (
|
||||||
<ProtectRoute>
|
<ProtectRoute permission='editor'>
|
||||||
<Editor />
|
<Editor />
|
||||||
</ProtectRoute>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { memo, useCallback, useEffect } from 'react';
|
||||||
import { VStack } from '@chakra-ui/react';
|
import { VStack } from '@chakra-ui/react';
|
||||||
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
||||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||||
@@ -44,7 +44,7 @@ const buttonStyle = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MenuBar(props: MenuBarProps) {
|
const MenuBar = (props: MenuBarProps) => {
|
||||||
const {
|
const {
|
||||||
isSettingsOpen,
|
isSettingsOpen,
|
||||||
onSettingsOpen,
|
onSettingsOpen,
|
||||||
@@ -175,4 +175,6 @@ export default function MenuBar(props: MenuBarProps) {
|
|||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default memo(MenuBar);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function OscIntegration() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
|
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
|
||||||
{subscriptionKeys.map((cycle, idx) => {
|
{subscriptionKeys.map((cycle, idx) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div key={`${cycle}-${idx}`}>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
key={cycle}
|
key={cycle}
|
||||||
cycle={cycle as TimerLifeCycle}
|
cycle={cycle as TimerLifeCycle}
|
||||||
@@ -118,7 +118,7 @@ export default function OscIntegration() {
|
|||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
|
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<OntimeModalFooter
|
<OntimeModalFooter
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function OscSettings() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<div style={{ height: '16px' }} />
|
||||||
<div className={styles.splitSection}>
|
<div className={styles.splitSection}>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
|
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||||
@@ -136,14 +136,14 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
hasCursor ? style.hasCursor : null,
|
hasCursor ? style.hasCursor : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handleFocusClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
moveCursorTo(eventId, true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
|
<div className={blockClasses} ref={setNodeRef} style={dragStyle} onClick={handleFocusClick}>
|
||||||
<div
|
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||||
className={style.binder}
|
|
||||||
style={{ ...binderColours }}
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={() => moveCursorTo(eventId, true)}
|
|
||||||
>
|
|
||||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+2
-2
@@ -5,10 +5,10 @@ import TableWrapper from './TableWrapper';
|
|||||||
|
|
||||||
export default function ProtectedTable() {
|
export default function ProtectedTable() {
|
||||||
return (
|
return (
|
||||||
<ProtectRoute>
|
<ProtectRoute permission='operator'>
|
||||||
<TableSettingsProvider>
|
<TableSettingsProvider>
|
||||||
<TableWrapper />
|
<TableWrapper />
|
||||||
</TableSettingsProvider>
|
</TableSettingsProvider>
|
||||||
</ProtectRoute>
|
</ProtectRoute>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0-beta5",
|
"version": "2.0.0-beta6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "2.0.0-beta5",
|
"version": "2.0.0-beta6",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.0",
|
"body-parser": "^1.20.0",
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { validateOscSubscription } from '../utils/parserFunctions.js';
|
|||||||
export const viewValidator = [
|
export const viewValidator = [
|
||||||
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
|
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
|
||||||
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
|
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
|
||||||
|
check('normalColor').isString().trim().withMessage('normalColor value must be string'),
|
||||||
|
check('warningColor').isString().trim().withMessage('warningColor value must be string'),
|
||||||
|
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
||||||
|
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
||||||
|
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { deleteFile, makeString, validateDuration } from './parserUtils.js';
|
|||||||
import {
|
import {
|
||||||
parseAliases,
|
parseAliases,
|
||||||
parseEventData,
|
parseEventData,
|
||||||
parseHttp,
|
|
||||||
parseOsc,
|
parseOsc,
|
||||||
parseRundown,
|
parseRundown,
|
||||||
parseSettings,
|
parseSettings,
|
||||||
@@ -282,7 +281,7 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
|
|||||||
// @ts-expect-error -- we are unable to type just yet
|
// @ts-expect-error -- we are unable to type just yet
|
||||||
returnData.osc = parseOsc(jsonData, enforce);
|
returnData.osc = parseOsc(jsonData, enforce);
|
||||||
// Import HTTP settings if any
|
// Import HTTP settings if any
|
||||||
returnData.http = parseHttp(jsonData, enforce);
|
// returnData.http = parseHttp(jsonData, enforce);
|
||||||
|
|
||||||
return returnData as DatabaseModel;
|
return returnData as DatabaseModel;
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-36
@@ -39,12 +39,18 @@
|
|||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"serverPort": 4001,
|
"serverPort": 4001,
|
||||||
"lock": null,
|
"editorKey": null,
|
||||||
"pinCode": "1234",
|
"operatorKey": null,
|
||||||
"timeFormat": "24"
|
"timeFormat": "24",
|
||||||
|
"language": "en"
|
||||||
},
|
},
|
||||||
"viewSettings": {
|
"viewSettings": {
|
||||||
"overrideStyles": false,
|
"overrideStyles": false,
|
||||||
|
"normalColor": "#ffffffcc",
|
||||||
|
"warningColor": "#FFAB33",
|
||||||
|
"warningThreshold": 120000,
|
||||||
|
"dangerColor": "#ED3333",
|
||||||
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [
|
"aliases": [
|
||||||
@@ -87,38 +93,5 @@
|
|||||||
],
|
],
|
||||||
"onFinish": []
|
"onFinish": []
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"http": {
|
|
||||||
"user": null,
|
|
||||||
"pwd": null,
|
|
||||||
"messages": {
|
|
||||||
"onLoad": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"onStart": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"onUpdate": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"onPause": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"onStop": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"onFinish": {
|
|
||||||
"url": "",
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0-beta5",
|
"version": "2.0.0-beta6",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
|
|||||||
Reference in New Issue
Block a user