Sheet use limited input device auth flow (#782)

* refactor: limited-input-device auth

* refactor: resolve sheet directory from setup

* refactor: extract sheet logic in backend

* refactor: simplify sheet integration

---------

Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2024-02-24 11:58:50 +01:00
committed by GitHub
parent fc5338903b
commit 474f1e2177
54 changed files with 1636 additions and 1144 deletions
@@ -0,0 +1,72 @@
import { useState } from 'react';
import { IconButton } from '@chakra-ui/react';
import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { CustomField, CustomFieldLabel } from 'ontime-types';
import Swatch from '../../../../common/components/input/colour-input/Swatch';
import CustomFieldForm from './CustomFieldForm';
import style from './ProjectSettingsPanel.module.scss';
interface CustomFieldEntryProps {
colour: string;
label: string;
onEdit: (label: CustomFieldLabel, patch: CustomField) => Promise<void>;
onDelete: (label: CustomFieldLabel) => Promise<void>;
}
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
const { colour, label, onEdit, onDelete } = props;
const [isEditing, setIsEditing] = useState(false);
const handleEdit = async (patch: CustomField) => {
const oldLabel = label;
await onEdit(oldLabel, patch);
setIsEditing(false);
};
if (isEditing) {
return (
<tr>
<td colSpan={99}>
<CustomFieldForm
onCancel={() => setIsEditing(false)}
onSubmit={handleEdit}
initialColour={colour}
initialLabel={label}
/>
</td>
</tr>
);
}
return (
<tr>
<td>
<Swatch color={colour} />
</td>
<td className={style.fullWidth}>{label}</td>
<td className={style.actions}>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoPencil />}
aria-label='Edit entry'
onClick={() => setIsEditing(true)}
/>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
onClick={() => onDelete(label)}
/>
</td>
</tr>
);
}
@@ -0,0 +1,99 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@chakra-ui/react';
import { CustomField } from 'ontime-types';
import { isAlphanumeric } from 'ontime-utils';
import { maybeAxiosError } from '../../../../common/api/apiUtils';
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
import * as Panel from '../PanelUtils';
import style from './ProjectSettingsPanel.module.scss';
interface CustomFieldsFormProps {
onSubmit: (field: CustomField) => Promise<void>;
onCancel: () => void;
initialColour?: string;
initialLabel?: string;
}
export default function CustomFieldForm(props: CustomFieldsFormProps) {
const { onSubmit, onCancel, initialColour, initialLabel } = props;
const {
handleSubmit,
register,
setFocus,
setError,
setValue,
getValues,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm({
defaultValues: { label: initialLabel || '', colour: initialColour || '' },
resetOptions: {
keepDirtyValues: true,
},
});
const setupSubmit = async (values: { label: string; colour: string }) => {
const { label, colour } = values;
const newField: CustomField = {
type: 'string', // type is not user definable yet
colour,
label,
};
try {
await onSubmit(newField);
} catch (error) {
setError('root', { type: 'custom', message: maybeAxiosError(error) });
}
};
// give initial focus to the label
useEffect(() => {
setFocus('label');
}, [setFocus]);
const handleSelectColour = (colour: string) => {
setValue('colour', colour, { shouldDirty: true });
};
const colour = getValues('colour');
const canSubmit = isDirty && isValid;
return (
<form onSubmit={handleSubmit(setupSubmit)} className={style.fieldForm}>
<div className={style.column}>
<Panel.Description>Label</Panel.Description>
{errors.label && <Panel.Error>{errors.label.message}</Panel.Error>}
<Input
{...register('label', {
required: { value: true, message: 'Required field' },
validate: (value) => {
if (value.trim().length === 0) return 'Required field';
if (!isAlphanumeric(value)) return 'Only alphanumeric characters are allowed';
return true;
},
})}
size='sm'
variant='ontime-filled'
autoComplete='off'
/>
</div>
<div>
<Panel.Description>Colour</Panel.Description>
<SwatchSelect name='colour' value={colour} handleChange={(_field, value) => handleSelectColour(value)} />
</div>
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<div className={style.buttonRow}>
<Button size='sm' variant='ontime-ghosted' onClick={onCancel}>
Cancel
</Button>
<Button size='sm' type='submit' variant='ontime-filled' isDisabled={!canSubmit} isLoading={isSubmitting}>
Save
</Button>
</div>
</form>
);
}
@@ -1,8 +1,22 @@
.fullWidth {
width: 100%;
width: 100%;
}
.actions {
display: flex;
gap: 0.5rem;
display: flex;
gap: 0.5rem;
}
.fieldForm {
padding: 1rem;
background-color: $gray-1350;
display: flex;
flex-direction: column;
gap: 1rem;
}
.buttonRow {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
@@ -1,74 +1,94 @@
import { Alert, AlertDescription, AlertIcon, IconButton } from '@chakra-ui/react';
import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
import { useState } from 'react';
import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { CustomField, CustomFieldLabel } from 'ontime-types';
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/ontimeApi';
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
import * as Panel from '../PanelUtils';
import style from './ProjectSettingsPanel.module.scss';
const demoCustomFields = {
Apple: { value: 'Fruit' },
Dog: { value: 'Animal' },
Sun: { value: 'Star' },
Car: { value: 'Vehicle' },
Tree: { value: 'Plant' },
Bird: { value: 'Creature' },
Book: { value: 'Reading' },
Chair: { value: 'Furniture' },
Music: { value: 'Melody' },
Ocean: { value: 'Sea' },
};
import CustomFieldEntry from './CustomFieldEntry';
import CustomFieldForm from './CustomFieldForm';
const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
export default function ProjectSettingsPanel() {
const { data, refetch } = useCustomFields();
const [isAdding, setIsAdding] = useState(false);
const handleInitiateCreate = () => {
setIsAdding(true);
};
const handleCancel = () => {
setIsAdding(false);
};
const handleCreate = async (customField: CustomField) => {
await postCustomField(customField);
refetch();
setIsAdding(false);
};
const handleEditField = async (label: CustomFieldLabel, customField: CustomField) => {
await editCustomField(label, customField);
refetch();
};
const handleDelete = async (label: string) => {
try {
await deleteCustomField(label);
refetch();
} catch (_error) {
/** we do not handle errors here */
}
};
return (
<>
<Panel.Header>Project Settings</Panel.Header>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Custom fields</Panel.SubHeader>
<div>
<Panel.SubHeader>
Custom fields
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleInitiateCreate}>
New
</Button>
</Panel.SubHeader>
<Panel.Section>
<Alert status='info' variant='ontime-on-dark-info'>
<AlertIcon />
<AlertDescription>
Custom fields allow for additional information to be added to an event (eg. light, sound, camera).{' '}
<br />
This data is not used by Ontime. <br />
<br />
This data is not used by Ontime.
<ExternalLink href={userFieldsDocsUrl}>See the docs</ExternalLink>
</AlertDescription>
</Alert>
</div>
</Panel.Section>
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
<Panel.Table>
<thead>
<tr>
<th>Colour</th>
<th>Name</th>
<th />
</tr>
</thead>
<tbody>
{Object.entries(demoCustomFields).map(([key, { value }]) => (
<tr key={key}>
<td className={style.fullWidth}>{value}</td>
<td className={style.actions}>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoPencil />}
aria-label='Edit entry'
/>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
/>
</td>
</tr>
))}
{Object.entries(data).map(([key, { colour, label }]) => {
return (
<CustomFieldEntry
key={key}
colour={colour}
label={label}
onEdit={handleEditField}
onDelete={handleDelete}
/>
);
})}
</tbody>
</Panel.Table>
</Panel.Card>