mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-23 16:09:11 +00:00
refactor: improve errors to user
This commit is contained in:
committed by
Carlos Valente
parent
9f1a64f53c
commit
0335da9786
@@ -2,8 +2,6 @@ import { useEffect } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, Input } from '@chakra-ui/react';
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
export type ProjectFormValues = {
|
export type ProjectFormValues = {
|
||||||
@@ -15,10 +13,9 @@ interface ProjectFormProps {
|
|||||||
filename: string;
|
filename: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
||||||
submitError: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectForm({ action, filename, onSubmit, onCancel, submitError }: ProjectFormProps) {
|
export default function ProjectForm({ action, filename, onSubmit, onCancel }: ProjectFormProps) {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
@@ -37,34 +34,31 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel, subm
|
|||||||
}, [setFocus]);
|
}, [setFocus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form onSubmit={handleSubmit(onSubmit)} className={style.form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={style.form}>
|
<Input
|
||||||
<Input
|
className={style.formInput}
|
||||||
className={style.formInput}
|
id='filename'
|
||||||
id='filename'
|
size='sm'
|
||||||
|
type='text'
|
||||||
|
variant='ontime-filled'
|
||||||
|
placeholder='Enter new name'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('filename', { required: true })}
|
||||||
|
/>
|
||||||
|
<div className={style.actionButtons}>
|
||||||
|
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
type='text'
|
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
placeholder='Enter new name'
|
isDisabled={!isDirty || !isValid || isSubmitting}
|
||||||
autoComplete='off'
|
type='submit'
|
||||||
{...register('filename')}
|
className={style.saveButton}
|
||||||
/>
|
>
|
||||||
<div className={style.actionButtons}>
|
{action}
|
||||||
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
|
</Button>
|
||||||
Cancel
|
</div>
|
||||||
</Button>
|
</form>
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
isDisabled={!isDirty || !isValid || isSubmitting}
|
|
||||||
type='submit'
|
|
||||||
className={style.saveButton}
|
|
||||||
>
|
|
||||||
{action}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{submitError && <Panel.Error>{submitError}</Panel.Error>}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||||
|
|
||||||
@@ -40,36 +41,53 @@ export default function ProjectListItem({
|
|||||||
onToggleEditMode,
|
onToggleEditMode,
|
||||||
}: ProjectListItemProps) {
|
}: ProjectListItemProps) {
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmitRename = async (values: ProjectFormValues) => {
|
const handleSubmitAction = (actionType: 'rename' | 'duplicate') => {
|
||||||
try {
|
return async (values: ProjectFormValues) => {
|
||||||
|
setLoading(true);
|
||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
|
try {
|
||||||
if (!values.filename) {
|
if (!values.filename) {
|
||||||
setSubmitError('Filename cannot be blank');
|
setSubmitError('Filename cannot be blank');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const action = actionType === 'rename' ? renameProject : duplicateProject;
|
||||||
|
await action(filename, values.filename);
|
||||||
|
await onRefetch();
|
||||||
|
onSubmit();
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
await renameProject(filename, values.filename);
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoad = async (filename: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
|
try {
|
||||||
|
await loadProject(filename);
|
||||||
await onRefetch();
|
await onRefetch();
|
||||||
onSubmit();
|
await invalidateAllCaches();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSubmitError(maybeAxiosError(error));
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitDuplicate = async (values: ProjectFormValues) => {
|
const handleDelete = async (filename: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
try {
|
try {
|
||||||
setSubmitError(null);
|
await deleteProject(filename);
|
||||||
|
|
||||||
if (!values.filename) {
|
|
||||||
setSubmitError('Filename cannot be blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await duplicateProject(filename, values.filename);
|
|
||||||
await onRefetch();
|
await onRefetch();
|
||||||
onSubmit();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSubmitError(maybeAxiosError(error));
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,50 +104,55 @@ export default function ProjectListItem({
|
|||||||
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
|
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={filename} className={classes}>
|
<>
|
||||||
{isCurrentlyBeingEdited ? (
|
{submitError && (
|
||||||
<td colSpan={99}>
|
<tr key='filename-error'>
|
||||||
<ProjectForm
|
<td colSpan={99}>
|
||||||
action={editingMode}
|
<Panel.Error>{submitError}</Panel.Error>
|
||||||
filename={filename}
|
</td>
|
||||||
onSubmit={editingMode === 'duplicate' ? handleSubmitDuplicate : handleSubmitRename}
|
</tr>
|
||||||
onCancel={handleCancel}
|
)}
|
||||||
submitError={submitError}
|
<tr key={filename} className={classes}>
|
||||||
/>
|
{isCurrentlyBeingEdited ? (
|
||||||
</td>
|
<td colSpan={99}>
|
||||||
) : (
|
<ProjectForm
|
||||||
<>
|
action={editingMode}
|
||||||
<td className={style.containCell}>{filename}</td>
|
|
||||||
<td>{new Date(updatedAt).toLocaleString()}</td>
|
|
||||||
<td className={style.actionButton}>
|
|
||||||
<ActionMenu
|
|
||||||
current={current}
|
|
||||||
filename={filename}
|
filename={filename}
|
||||||
onChangeEditMode={handleToggleEditMode}
|
onSubmit={editingMode === 'duplicate' ? handleSubmitAction('duplicate') : handleSubmitAction('rename')}
|
||||||
onRefetch={onRefetch}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</>
|
) : (
|
||||||
)}
|
<>
|
||||||
</tr>
|
<td className={style.containCell}>{filename}</td>
|
||||||
|
<td>{new Date(updatedAt).toLocaleString()}</td>
|
||||||
|
<td className={style.actionButton}>
|
||||||
|
<ActionMenu
|
||||||
|
current={current}
|
||||||
|
filename={filename}
|
||||||
|
onChangeEditMode={handleToggleEditMode}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
isDisabled={loading}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionMenu({
|
interface ActionMenuProps {
|
||||||
current,
|
|
||||||
filename,
|
|
||||||
onChangeEditMode,
|
|
||||||
onRefetch,
|
|
||||||
}: {
|
|
||||||
current?: boolean;
|
current?: boolean;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
isDisabled: boolean;
|
||||||
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
||||||
onRefetch: () => Promise<void>;
|
onDelete: (filename: string) => void;
|
||||||
}) {
|
onLoad: (filename: string) => void;
|
||||||
const handleLoad = async () => {
|
}
|
||||||
await loadProject(filename);
|
function ActionMenu(props: ActionMenuProps) {
|
||||||
await invalidateAllCaches();
|
const { current, filename, isDisabled, onChangeEditMode, onDelete, onLoad } = props;
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
onChangeEditMode('rename', filename);
|
onChangeEditMode('rename', filename);
|
||||||
@@ -139,11 +162,6 @@ function ActionMenu({
|
|||||||
onChangeEditMode('duplicate', filename);
|
onChangeEditMode('duplicate', filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteProject(filename);
|
|
||||||
await onRefetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
await downloadProject(filename);
|
await downloadProject(filename);
|
||||||
};
|
};
|
||||||
@@ -161,16 +179,17 @@ function ActionMenu({
|
|||||||
color='#e2e2e2' // $gray-200
|
color='#e2e2e2' // $gray-200
|
||||||
variant='ontime-ghosted'
|
variant='ontime-ghosted'
|
||||||
size='sm'
|
size='sm'
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem onClick={handleLoad} isDisabled={current}>
|
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
||||||
Load
|
Load
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
||||||
<MenuItem isDisabled={current} onClick={handleDelete}>
|
<MenuItem isDisabled={current} onClick={() => onDelete(filename)}>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
|||||||
Reference in New Issue
Block a user