mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
Move app settings (#765)
* refactor: migrate app settings --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
@@ -3,6 +3,7 @@ import { ErrorBoundary } from '@sentry/react';
|
||||
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||
|
||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||
import LogPanel from './panel/log-panel/LogPanel';
|
||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
@@ -29,6 +30,7 @@ export default function AppSettings() {
|
||||
<PanelList />
|
||||
<PanelContent onClose={closeSettings}>
|
||||
{selectedPanel === 'project' && <ProjectPanel />}
|
||||
{selectedPanel === 'general' && <GeneralPanel />}
|
||||
{selectedPanel === 'sources' && <SourcesPanel />}
|
||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import GeneralPanelForm from './GeneralPanelForm';
|
||||
|
||||
export default function GeneralPanel() {
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Settings</Panel.Header>
|
||||
<GeneralPanelForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button, Input, Select } from '@chakra-ui/react';
|
||||
import { Settings } from 'ontime-types';
|
||||
|
||||
import { postSettings } from '../../../../common/api/settings';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import GeneralPinInput from './GeneralPinInput';
|
||||
|
||||
import style from './GeneralPanel.module.scss';
|
||||
|
||||
export type GeneralPanelFormValues = {
|
||||
filename: string;
|
||||
};
|
||||
|
||||
export default function GeneralPanelForm() {
|
||||
const { data, status, refetch } = useSettings();
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isSubmitting, isDirty, isValid, errors },
|
||||
} = useForm<Settings>({
|
||||
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: Settings) => {
|
||||
try {
|
||||
await postSettings(formData);
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
} finally {
|
||||
await refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const disableInputs = status === 'pending';
|
||||
const disableSubmit = isSubmitting || !isDirty || !isValid;
|
||||
const submitError = '';
|
||||
|
||||
const onReset = () => {
|
||||
reset(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} id='app-settings'>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
General settings
|
||||
<div className={style.actionButtons}>
|
||||
<Button isDisabled={!isDirty || isSubmitting} variant='ontime-ghost' size='sm' onClick={onReset}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
form='app-settings'
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={disableSubmit}
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
{submitError && <Panel.Error>{submitError}</Panel.Error>}
|
||||
<Panel.Divider />
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Ontime server port'
|
||||
description='Port ontime server listens in. Defaults to 4001 (needs app restart)'
|
||||
error={errors.serverPort?.message}
|
||||
/>
|
||||
<Input
|
||||
id='serverPort'
|
||||
size='sm'
|
||||
type='number'
|
||||
variant='ontime-filled'
|
||||
maxLength={5}
|
||||
width='75px'
|
||||
{...register('serverPort', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Editor pin code'
|
||||
description='Protect the editor view with a pin code'
|
||||
error={errors.editorKey?.message}
|
||||
/>
|
||||
<GeneralPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Operator pin code'
|
||||
description='Protect the operator and cuesheet views with a pin code'
|
||||
error={errors.operatorKey?.message}
|
||||
/>
|
||||
<GeneralPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Time format'
|
||||
description='Default time format to show in views 12 /24 hours'
|
||||
error={errors.timeFormat?.message}
|
||||
/>
|
||||
<Select variant='ontime' size='sm' width='auto' isDisabled={disableInputs} {...register('timeFormat')}>
|
||||
<option value='12'>12 hours 11:00:10 PM</option>
|
||||
<option value='24'>24 hours 23:00:10</option>
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Views language'
|
||||
description='Language to be displayed in views'
|
||||
error={errors.language?.message}
|
||||
/>
|
||||
<Select variant='ontime' size='sm' width='auto' isDisabled={disableInputs} {...register('language')}>
|
||||
<option value='en'>English</option>
|
||||
<option value='fr'>French</option>
|
||||
<option value='de'>German</option>
|
||||
<option value='it'>Italian</option>
|
||||
<option value='no'>Norwegian</option>
|
||||
<option value='pt'>Portuguese</option>
|
||||
<option value='es'>Spanish</option>
|
||||
<option value='sv'>Swedish</option>
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PropsWithChildren, 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 { Settings } from 'ontime-types';
|
||||
|
||||
interface GeneralPinInputProps {
|
||||
register: UseFormRegister<Settings>;
|
||||
formName: keyof Settings;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInputProps>) {
|
||||
const { register, formName, isDisabled } = props;
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' width='100px'>
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
maxLength={4}
|
||||
{...register(formName)}
|
||||
placeholder='-'
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
onMouseDown={() => setVisible(true)}
|
||||
onMouseUp={() => setVisible(false)}
|
||||
size='sm'
|
||||
variant='ontime-ghosted'
|
||||
icon={<IoEyeOutline />}
|
||||
aria-label='Show pin code'
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
||||
label: 'Project',
|
||||
secondary: [{ id: 'project__manage', label: 'Manage project files' }],
|
||||
},
|
||||
{ id: 'general', label: 'General', secondary: [{ id: 'general__manage', label: 'Manage app settings' }] },
|
||||
{
|
||||
id: 'project_settings',
|
||||
label: 'Project Settings',
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input, Select } from '@chakra-ui/react';
|
||||
import type { Settings } from 'ontime-types';
|
||||
|
||||
import { postSettings } from '../../../common/api/settings';
|
||||
import { logAxiosError } from '../../../common/api/utils';
|
||||
import useSettings from '../../../common/hooks-query/useSettings';
|
||||
import { isOnlyNumbers } from '../../../common/utils/regex';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import ModalSplitInput from '../ModalSplitInput';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
|
||||
import ModalPinInput from './ModalPinInput';
|
||||
|
||||
import style from './SettingsModal.module.scss';
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, isFetching, refetch } = useSettings();
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<Settings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: Settings) => {
|
||||
try {
|
||||
await postSettings(formData);
|
||||
} catch (error) {
|
||||
logAxiosError('Error saving settings', error);
|
||||
} finally {
|
||||
await refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
reset(data);
|
||||
};
|
||||
|
||||
const disableInputs = status === 'pending';
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='app-settings' className={style.sectionContainer}>
|
||||
<ModalSplitInput
|
||||
field='serverPort'
|
||||
title='Ontime is available on port'
|
||||
description='Default 4001 (needs app restart to change)'
|
||||
error={errors.serverPort?.message}
|
||||
>
|
||||
<Input
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
maxLength={5}
|
||||
variant='ontime-filled-on-light'
|
||||
{...register('serverPort', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput
|
||||
field='editorKey'
|
||||
title='Editor pin code'
|
||||
description='Protect the editor with a pin code'
|
||||
error={errors.editorKey?.message}
|
||||
>
|
||||
<ModalPinInput register={register as any} formName='editorKey' isDisabled={disableInputs} />
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput
|
||||
field='operatorKey'
|
||||
title='Operator pin code'
|
||||
description='Protect the cuesheet with a pin code'
|
||||
error={errors.operatorKey?.message}
|
||||
>
|
||||
<ModalPinInput register={register as any} formName='operatorKey' isDisabled={disableInputs} />
|
||||
</ModalSplitInput>
|
||||
<div style={{ height: '16px' }} />
|
||||
<ModalSplitInput
|
||||
field='timeFormat'
|
||||
title='Time Format'
|
||||
description='Views 12 / 24 hours'
|
||||
error={errors.timeFormat?.message}
|
||||
>
|
||||
<Select backgroundColor='white' size='sm' width='auto' isDisabled={disableInputs} {...register('timeFormat')}>
|
||||
<option value='12'>12 hours eg. 11:00:10 PM</option>
|
||||
<option value='24'>24 hours eg. 23:00:10</option>
|
||||
</Select>
|
||||
</ModalSplitInput>
|
||||
<ModalSplitInput
|
||||
field='language'
|
||||
title='Views Language'
|
||||
description='Language for static fields in views'
|
||||
error={errors.language?.message}
|
||||
>
|
||||
<Select backgroundColor='white' size='sm' width='auto' isDisabled={disableInputs} {...register('language')}>
|
||||
<option value='en'>English</option>
|
||||
<option value='fr'>French</option>
|
||||
<option value='de'>German</option>
|
||||
<option value='it'>Italian</option>
|
||||
<option value='no'>Norwegian</option>
|
||||
<option value='pt'>Portuguese</option>
|
||||
<option value='es'>Spanish</option>
|
||||
<option value='sv'>Swedish</option>
|
||||
</Select>
|
||||
</ModalSplitInput>
|
||||
<OntimeModalFooter
|
||||
formId='app-settings'
|
||||
handleRevert={onReset}
|
||||
isDirty={isDirty}
|
||||
isValid={isValid}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/r
|
||||
import ModalWrapper from '../ModalWrapper';
|
||||
|
||||
import AliasesForm from './AliasesForm';
|
||||
import AppSettingsModal from './AppSettings';
|
||||
import EditorSettings from './EditorSettings';
|
||||
import ProjectDataForm from './ProjectDataForm';
|
||||
import ViewSettingsForm from './ViewSettingsForm';
|
||||
@@ -20,16 +19,12 @@ export default function SettingsModal(props: ModalManagerProps) {
|
||||
<ModalBody>
|
||||
<Tabs variant='ontime' size='sm' isLazy>
|
||||
<TabList>
|
||||
<Tab>App</Tab>
|
||||
<Tab>Project Data</Tab>
|
||||
<Tab>Editor</Tab>
|
||||
<Tab>Views</Tab>
|
||||
<Tab>URL Aliases</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<AppSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ProjectDataForm />
|
||||
</TabPanel>
|
||||
|
||||
Reference in New Issue
Block a user