chore: migrate remaining settings (#799)

* chore: migrate remaining settings

---------

Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com>
This commit is contained in:
Carlos Valente
2024-03-03 19:16:25 +01:00
committed by GitHub
parent 10d3986832
commit 0c31438e44
18 changed files with 561 additions and 318 deletions
@@ -0,0 +1,45 @@
$loader-size: 4rem;
.overlay {
position: absolute;
z-index: 10;
width: 100%;
height: 100%;
display: grid;
place-content: center;
background-color: $black-10;
backdrop-filter: blur(5px);
}
.loader {
width: $loader-size;
height: $loader-size;
background: $blue-500;
display: inline-block;
border-radius: 50%;
box-sizing: border-box;
animation: animloader 1s ease-in infinite;
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@@ -0,0 +1,9 @@
import style from './LoaderOverlay.module.scss';
export default function LoaderOverlay() {
return (
<div className={style.overlay}>
<span className={style.loader} />
</div>
);
}
@@ -17,5 +17,5 @@ export default function useInfo() {
networkMode: 'always',
});
return { data, status, isError, refetch, isFetching };
return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching };
}
@@ -27,6 +27,7 @@ $inner-padding: 1rem;
}
.section {
position: relative;
margin-top: 2rem;
font-size: calc(1rem - 1px);
max-width: 800px;
@@ -128,3 +129,49 @@ $inner-padding: 1rem;
border-top: 1px solid $white-10;
margin: 1rem -2rem;
}
$loader-size: 4rem;
.overlay {
position: absolute;
z-index: 10;
width: calc(100% - 2rem);
left: 1rem;
height: calc(100% - 1rem);
display: grid;
place-content: center;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.01);
}
.loader {
width: $loader-size;
height: $loader-size;
background: $blue-500;
display: inline-block;
border-radius: 50%;
box-sizing: border-box;
animation: animloader 1s ease-in infinite;
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@@ -81,3 +81,11 @@ export function Error({ children }: { children: ReactNode }) {
export function Divider() {
return <hr className={style.divider} />;
}
export function Loader() {
return (
<div className={style.overlay}>
<span className={style.loader} />
</div>
);
}
@@ -0,0 +1,55 @@
import { Switch } from '@chakra-ui/react';
import { useEditorSettings } from '../../../../common/stores/editorSettings';
import * as Panel from '../PanelUtils';
export default function EditorSettingsForm() {
const eventSettings = useEditorSettings((state) => state.eventSettings);
const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry);
const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd);
const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Editor settings</Panel.SubHeader>
<Panel.Divider />
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show under selected event'
/>
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.showQuickEntry}
onChange={(event) => setShowQuickEntry(event.target.checked)}
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Start time is last end'
description='New events start time will be the previous event end'
/>
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.startTimeIsLastEnd}
onChange={(event) => setStartTimeIsLastEnd(event.target.checked)}
/>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Default public' description='New events will be public' />
<Switch
variant='ontime'
size='lg'
defaultChecked={eventSettings.defaultPublic}
onChange={(event) => setDefaultPublic(event.target.checked)}
/>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Card>
</Panel.Section>
);
}
@@ -1,12 +1,16 @@
import * as Panel from '../PanelUtils';
import EditorSettingsForm from './EditorSettingsForm';
import GeneralPanelForm from './GeneralPanelForm';
import ViewSettingsForm from './ViewSettingsForm';
export default function GeneralPanel() {
return (
<>
<Panel.Header>Settings</Panel.Header>
<GeneralPanelForm />
<EditorSettingsForm />
<ViewSettingsForm />
</>
);
}
@@ -65,7 +65,7 @@ export default function GeneralPanelForm() {
<Panel.SubHeader>
General settings
<div className={style.actionButtons}>
<Button isDisabled={!isDirty || isSubmitting} variant='ontime-ghost' size='sm' onClick={onReset}>
<Button isDisabled={!isDirty || isSubmitting} variant='ontime-ghosted' size='sm' onClick={onReset}>
Revert to saved
</Button>
<Button
@@ -0,0 +1,136 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertIcon, Button, Input, Switch } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import { maybeAxiosError } from '../../../../common/api/utils';
import { postViewSettings } from '../../../../common/api/viewSettings';
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
import { PopoverPickerRHF } from '../../../../common/components/input/popover-picker/PopoverPicker';
import useInfo from '../../../../common/hooks-query/useInfo';
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
import * as Panel from '../PanelUtils';
import style from './GeneralPanel.module.scss';
const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling';
export default function ViewSettingsForm() {
const { data, status, refetch, isFetching } = useViewSettings();
const { data: info, isFetching: isFetchingInfo } = useInfo();
const {
control,
handleSubmit,
register,
reset,
setError,
formState: { isSubmitting, isDirty },
} = useForm<ViewSettings>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
// update form if we get new data from server
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: ViewSettings) => {
const newData = {
...formData,
};
try {
await postViewSettings(newData);
} catch (error) {
const message = maybeAxiosError(error);
setError('root', { message });
} finally {
await refetch();
}
};
const onReset = () => {
reset(data);
};
if (!control) {
return null;
}
const disableInputs = status === 'pending';
return (
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} id='view-settings'>
<Panel.Card>
<Panel.SubHeader>
View settings
<div className={style.actionButtons}>
<Button isDisabled={!isDirty} variant='ontime-ghosted' size='sm' onClick={onReset}>
Revert to saved
</Button>
<Button type='submit' isLoading={isSubmitting} isDisabled={!isDirty} variant='ontime-filled' size='sm'>
Save
</Button>
</div>
</Panel.SubHeader>
<Panel.Divider />
{isFetching || isFetchingInfo ? <Panel.Loader /> : null}
<Panel.ListGroup>
<Alert status='info' variant='ontime-on-dark-info'>
<AlertIcon />
<AlertDescription>
You can override the styles of the viewers with a custom CSS file. <br />
{info?.cssOverride && `In your installation the file is at ${info?.cssOverride}`}
<br />
<br />
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
</AlertDescription>
</Alert>
<Panel.ListItem>
<Panel.Field
title='Override CSS styles'
description='Enables overriding view styles with custom stylesheet'
/>
<Switch {...register('overrideStyles')} variant='ontime' size='lg' />
</Panel.ListItem>
</Panel.ListGroup>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='Timer colour' description='Default colour of a running timer' />
<PopoverPickerRHF name='normalColor' control={control} />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Warning colour' description='Colour of a running timer in warning mode' />
<PopoverPickerRHF name='warningColor' control={control} />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Danger colour' description='Colour of a running timer in danger mode' />
<PopoverPickerRHF name='dangerColor' control={control} />
</Panel.ListItem>
</Panel.ListGroup>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='End message' description='If no end message is provided, timer will continue' />
<Input
size='sm'
autoComplete='off'
variant='ontime-filled'
maxLength={150}
width='275px'
placeholder='Message shown when timer reaches end'
isDisabled={disableInputs}
{...register('endMessage')}
/>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Card>
</Panel.Section>
);
}
@@ -0,0 +1,95 @@
import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { uploadProjectFile } from '../../../../common/api/db';
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
import * as Panel from '../PanelUtils';
import ProjectCreateForm from './ProjectCreateForm';
import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss';
export default function ManageProjects() {
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev);
};
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target?.files?.[0];
if (!selectedFile) {
return;
}
setLoading('import');
try {
validateProjectFile(selectedFile);
await uploadProjectFile(selectedFile);
} catch (error) {
const errorMessage = maybeAxiosError(error);
setError(`Error uploading file: ${errorMessage}`);
} finally {
invalidateAllCaches();
}
setLoading(null);
};
const handleCloseForm = () => {
setIsCreatingProject(false);
};
return (
<Panel.Section>
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleImport}
accept='.json'
data-testid='file-input'
/>
<Panel.Card>
<Panel.SubHeader>
Manage projects
<div className={style.headerButtons}>
<Button
variant='ontime-subtle'
onClick={handleSelectFile}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
isLoading={loading === 'import'}
>
Import
</Button>
<Button
variant='ontime-subtle'
onClick={handleToggleCreate}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
rightIcon={<IoAdd />}
>
Add
</Button>
</div>
</Panel.SubHeader>
{error && <Panel.Error>{error}</Panel.Error>}
<Panel.Divider />
{isCreatingProject && <ProjectCreateForm onClose={handleCloseForm} />}
<ProjectList />
</Panel.Card>
</Panel.Section>
);
}
@@ -0,0 +1,155 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Textarea } from '@chakra-ui/react';
import { type ProjectData } from 'ontime-types';
import { postProjectData } from '../../../../common/api/project';
import { maybeAxiosError } from '../../../../common/api/utils';
import useProjectData from '../../../../common/hooks-query/useProjectData';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss';
export default function ProjectData() {
const { data, refetch } = useProjectData();
const {
handleSubmit,
register,
reset,
formState: { isSubmitting, isValid, isDirty },
setError,
} = useForm({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
// reset form values if data changes
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: ProjectData) => {
try {
await postProjectData(formData);
} catch (error) {
const message = maybeAxiosError(error);
setError('root', { message });
} finally {
await refetch();
}
};
const onReset = () => {
reset(data);
};
return (
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)}>
<Panel.Card>
<Panel.SubHeader>
Project Data
<div className={style.headerButtons}>
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={isSubmitting || !isDirty}>
Revert to saved
</Button>
<Button
variant='ontime-filled'
size='sm'
type='submit'
isDisabled={!isDirty || !isValid}
isLoading={isSubmitting}
>
Save
</Button>
</div>
</Panel.SubHeader>
<Panel.Divider />
<Panel.Section>
<div className={style.createFormInputField}>
<label>
Project title
<Input
variant='ontime-filled'
size='sm'
maxLength={50}
placeholder='Your project name'
autoComplete='off'
{...register('title')}
/>
</label>
</div>
<div className={style.createFormInputField}>
<label>
Project description
<Input
variant='ontime-filled'
size='sm'
maxLength={100}
placeholder='Euro Love, Malmö 2024'
autoComplete='off'
{...register('description')}
/>
</label>
</div>
<div className={style.createFormInputField}>
<label>
Public info
<Textarea
variant='ontime-filled'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
autoComplete='off'
resize='none'
{...register('publicInfo')}
/>
</label>
</div>
<div className={style.createFormInputField}>
<label>
Public QR code URL
<Input
variant='ontime-filled'
size='sm'
placeholder='www.getontime.no'
autoComplete='off'
{...register('publicUrl')}
/>
</label>
</div>
<div className={style.createFormInputField}>
<label>
Backstage info
<Textarea
variant='ontime-filled'
size='sm'
maxLength={150}
placeholder='Wi-Fi password: 1234'
autoComplete='off'
resize='none'
{...register('backstageInfo')}
/>
</label>
</div>
<div className={style.createFormInputField}>
<label>
Backstage QR code URL
<Input
variant='ontime-filled'
size='sm'
placeholder='www.ontime.gitbook.io'
autoComplete='off'
{...register('backstageUrl')}
/>
</label>
</div>
</Panel.Section>
</Panel.Card>
</Panel.Section>
);
}
@@ -1,98 +1,14 @@
import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { uploadProjectFile } from '../../../../common/api/db';
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
import * as Panel from '../PanelUtils';
import ProjectCreateForm from './ProjectCreateForm';
import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss';
import ManageProjects from './ManageProjects';
import ProjectData from './ProjectData';
export default function ProjectPanel() {
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev);
};
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target?.files?.[0];
if (!selectedFile) {
return;
}
setLoading('import');
try {
validateProjectFile(selectedFile);
await uploadProjectFile(selectedFile);
} catch (error) {
const errorMessage = maybeAxiosError(error);
setError(`Error uploading file: ${errorMessage}`);
} finally {
invalidateAllCaches();
}
setLoading(null);
};
const handleCloseForm = () => {
setIsCreatingProject(false);
};
return (
<>
<Panel.Header>Project</Panel.Header>
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleImport}
accept='.json'
data-testid='file-input'
/>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>
Manage projects
<div className={style.headerButtons}>
<Button
variant='ontime-subtle'
onClick={handleSelectFile}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
isLoading={loading === 'import'}
>
Import
</Button>
<Button
variant='ontime-subtle'
onClick={handleToggleCreate}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
rightIcon={<IoAdd />}
>
Add
</Button>
</div>
</Panel.SubHeader>
{error && <Panel.Error>{error}</Panel.Error>}
{isCreatingProject && <ProjectCreateForm onClose={handleCloseForm} />}
<ProjectList />
</Panel.Card>
</Panel.Section>
<ProjectData />
<ManageProjects />
</>
);
}
@@ -1,44 +0,0 @@
import { Switch } from '@chakra-ui/react';
import { useEditorSettings } from '../../../common/stores/editorSettings';
import ModalSplitInput from '../ModalSplitInput';
import style from './SettingsModal.module.scss';
export default function EditorSettings() {
const eventSettings = useEditorSettings((state) => state.eventSettings);
const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry);
const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd);
const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
return (
<div className={style.sectionContainer}>
<span className={style.title}>Rundown settings</span>
<ModalSplitInput field='' title='Show quick entry' description='Whether quick entry shows under selected event'>
<Switch
variant='ontime-on-light'
defaultChecked={eventSettings.showQuickEntry}
onChange={(event) => setShowQuickEntry(event.target.checked)}
/>
</ModalSplitInput>
<ModalSplitInput
field=''
title='Start time is last end'
description='New events start time will be previous event end'
>
<Switch
variant='ontime-on-light'
defaultChecked={eventSettings.startTimeIsLastEnd}
onChange={(event) => setStartTimeIsLastEnd(event.target.checked)}
/>
</ModalSplitInput>
<ModalSplitInput field='' title='Default public' description='New events will be public'>
<Switch
variant='ontime-on-light'
defaultChecked={eventSettings.defaultPublic}
onChange={(event) => setDefaultPublic(event.target.checked)}
/>
</ModalSplitInput>
</div>
);
}
@@ -1,41 +0,0 @@
import { useState } from 'react';
import { UseFormRegister } from 'react-hook-form';
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { IoEyeOutline } from '@react-icons/all-files/io5/IoEyeOutline';
import { enDash } from '../../../common/utils/styleUtils';
interface FormInput {
[key: string]: string;
}
interface ModalPinInputProps {
register: UseFormRegister<FormInput>;
formName: string;
isDisabled?: boolean;
}
export default function ModalPinInput({ register, formName, isDisabled }: ModalPinInputProps) {
const [isVisible, setVisible] = useState(false);
return (
<InputGroup size='sm' width='100px'>
<Input
type={isVisible ? 'text' : 'password'}
maxLength={4}
{...register(formName)}
placeholder={enDash}
isDisabled={isDisabled}
/>
<InputRightElement>
<IconButton
onMouseDown={() => setVisible(true)}
onMouseUp={() => setVisible(false)}
size='sm'
variant='ontime-ghost-on-light'
icon={<IoEyeOutline />}
aria-label='Show pin code'
/>
</InputRightElement>
</InputGroup>
);
}
@@ -3,9 +3,7 @@ import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/r
import ModalWrapper from '../ModalWrapper';
import AliasesForm from './AliasesForm';
import EditorSettings from './EditorSettings';
import ProjectDataForm from './ProjectDataForm';
import ViewSettingsForm from './ViewSettingsForm';
interface ModalManagerProps {
isOpen: boolean;
@@ -20,20 +18,12 @@ export default function SettingsModal(props: ModalManagerProps) {
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>Project Data</Tab>
<Tab>Editor</Tab>
<Tab>Views</Tab>
<Tab>URL Aliases</Tab>
</TabList>
<TabPanels>
<TabPanel>
<ProjectDataForm />
</TabPanel>
<TabPanel>
<EditorSettings />
</TabPanel>
<TabPanel>
<ViewSettingsForm />
</TabPanel>
<TabPanel>
<AliasesForm />
</TabPanel>
@@ -1,129 +0,0 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input, Switch } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import { logAxiosError } from '../../../common/api/utils';
import { postViewSettings } from '../../../common/api/viewSettings';
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
import useInfo from '../../../common/hooks-query/useInfo';
import useViewSettings from '../../../common/hooks-query/useViewSettings';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalInput from '../ModalInput';
import ModalLink from '../ModalLink';
import ModalSplitInput from '../ModalSplitInput';
import OntimeModalFooter from '../OntimeModalFooter';
import style from './SettingsModal.module.scss';
const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling';
export default function ViewSettingsForm() {
const { data, status, refetch, isFetching } = useViewSettings();
const { data: info, isFetching: isFetchingInfo } = useInfo();
const {
control,
handleSubmit,
register,
reset,
formState: { isSubmitting, isDirty, isValid },
} = useForm<ViewSettings>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: ViewSettings) => {
const newData = {
...formData,
};
try {
await postViewSettings(newData);
} catch (error) {
logAxiosError('Error saving view settings', error);
} finally {
await refetch();
}
};
const onReset = () => {
reset(data);
};
if (!control) {
return null;
}
const disableInputs = status === 'pending';
if (isFetching || isFetchingInfo) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='view-settings' className={style.sectionContainer}>
<span className={style.title}>General view settings</span>
<Alert status='info' variant='ontime-on-light-info'>
<AlertIcon />
<div className={style.column}>
<AlertTitle>CSS Override</AlertTitle>
<AlertDescription>
Ontime will use the CSS file at its install location. <br />
<span className={style.url}>{info?.cssOverride}</span>
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
</AlertDescription>
</div>
</Alert>
<ModalSplitInput
field='overrideStyles'
title='Override CSS Styles'
description='Enables overriding view styles with custom stylesheet'
>
<Switch {...register('overrideStyles')} variant='ontime-on-light' />
</ModalSplitInput>
<span className={style.title}>Timer view settings</span>
<ModalSplitInput field='normalColor' title='Timer color' description='Normal color of a running timer'>
<PopoverPickerRHF name='normalColor' control={control} />
</ModalSplitInput>
<ModalSplitInput field='warningColor' title='Warning color' description='Color of timer in warning mode'>
<PopoverPickerRHF name='warningColor' control={control} />
</ModalSplitInput>
<ModalSplitInput field='dangerColor' title='Timer color' description='Color of timer in danger mode'>
<PopoverPickerRHF name='dangerColor' control={control} />
</ModalSplitInput>
<div style={{ height: '16px' }} />
<ModalInput
field='endMessage'
title='End Message'
description='If no end message is provided, timer will continue in overtime mode'
>
<Input
{...inputProps}
variant='ontime-filled-on-light'
maxLength={150}
placeholder='Message to be shown when timer reaches end'
isDisabled={disableInputs}
{...register('endMessage')}
/>
</ModalInput>
<OntimeModalFooter
formId='view-settings'
handleRevert={onReset}
isDirty={isDirty}
isValid={isValid}
isSubmitting={isSubmitting}
/>
</form>
);
}
@@ -21,9 +21,7 @@ export async function postViewSettings(req: Request, res: Response<ViewSettings
endMessage: req.body?.endMessage || '',
normalColor: req.body.normalColor,
warningColor: req.body.warningColor,
warningThreshold: req.body.warningThreshold,
dangerColor: req.body.dangerColor,
dangerThreshold: req.body.dangerThreshold,
};
await DataProvider.setViewSettings(newData);
res.status(200).send(newData);
@@ -10,8 +10,7 @@ export const validateViewSettings = [
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: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });