mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 17:09:09 +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 { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||||
|
|
||||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||||
|
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||||
import LogPanel from './panel/log-panel/LogPanel';
|
import LogPanel from './panel/log-panel/LogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
@@ -29,6 +30,7 @@ export default function AppSettings() {
|
|||||||
<PanelList />
|
<PanelList />
|
||||||
<PanelContent onClose={closeSettings}>
|
<PanelContent onClose={closeSettings}>
|
||||||
{selectedPanel === 'project' && <ProjectPanel />}
|
{selectedPanel === 'project' && <ProjectPanel />}
|
||||||
|
{selectedPanel === 'general' && <GeneralPanel />}
|
||||||
{selectedPanel === 'sources' && <SourcesPanel />}
|
{selectedPanel === 'sources' && <SourcesPanel />}
|
||||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
||||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
{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',
|
label: 'Project',
|
||||||
secondary: [{ id: 'project__manage', label: 'Manage project files' }],
|
secondary: [{ id: 'project__manage', label: 'Manage project files' }],
|
||||||
},
|
},
|
||||||
|
{ id: 'general', label: 'General', secondary: [{ id: 'general__manage', label: 'Manage app settings' }] },
|
||||||
{
|
{
|
||||||
id: 'project_settings',
|
id: 'project_settings',
|
||||||
label: '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 ModalWrapper from '../ModalWrapper';
|
||||||
|
|
||||||
import AliasesForm from './AliasesForm';
|
import AliasesForm from './AliasesForm';
|
||||||
import AppSettingsModal from './AppSettings';
|
|
||||||
import EditorSettings from './EditorSettings';
|
import EditorSettings from './EditorSettings';
|
||||||
import ProjectDataForm from './ProjectDataForm';
|
import ProjectDataForm from './ProjectDataForm';
|
||||||
import ViewSettingsForm from './ViewSettingsForm';
|
import ViewSettingsForm from './ViewSettingsForm';
|
||||||
@@ -20,16 +19,12 @@ export default function SettingsModal(props: ModalManagerProps) {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Tabs variant='ontime' size='sm' isLazy>
|
<Tabs variant='ontime' size='sm' isLazy>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>App</Tab>
|
|
||||||
<Tab>Project Data</Tab>
|
<Tab>Project Data</Tab>
|
||||||
<Tab>Editor</Tab>
|
<Tab>Editor</Tab>
|
||||||
<Tab>Views</Tab>
|
<Tab>Views</Tab>
|
||||||
<Tab>URL Aliases</Tab>
|
<Tab>URL Aliases</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
|
||||||
<AppSettingsModal />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<ProjectDataForm />
|
<ProjectDataForm />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|||||||
Reference in New Issue
Block a user