mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 01:43:43 +00:00
chore: migrate remaining settings (#799)
* chore: migrate remaining settings --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com>
This commit is contained in:
@@ -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() });
|
||||
|
||||
Reference in New Issue
Block a user