Skip to content

Commit 27e2214

Browse files
committed
add error for form
1 parent 9f0bb35 commit 27e2214

File tree

7 files changed

+88
-33
lines changed

7 files changed

+88
-33
lines changed

react/src/store/generalSlice.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ export const createOrUpdateRow = createAsyncThunk<
4141
unknown,
4242
{ model: ModelType; id: number | string; data: Record<string, FieldValue>; message: string },
4343
{ rejectValue: string }
44-
>('models/create_or_update_row', async ({ model, id, data, message }, thunkApi: GetThunkAPI<{ rejectValue: string }>) => {
44+
>('models/create_or_update_row', async ({ model, id, data, message }, thunkApi: GetThunkAPI<{ rejectValue: unknown }>) => {
4545
try {
4646
const res = await API.post(`/${model}/${id}`, data);
4747
toast.success(message);
4848
return res.data;
4949
} catch (err: unknown) {
50-
const msg = err instanceof Error ? err.message : 'Failed to save row';
51-
return thunkApi.rejectWithValue(msg);
50+
return thunkApi.rejectWithValue(err);
5251
}
5352
});
5453

react/src/ui-component/cards/MainCard.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Ref } from 'react';
1+
import { CSSProperties, forwardRef, ReactNode, Ref } from 'react';
22
import { useTheme, SxProps, Theme } from '@mui/material/styles';
33
import { Card, CardContent, CardHeader, Divider, Typography, CardProps, CardHeaderProps, CardContentProps } from '@mui/material';
44

@@ -7,21 +7,21 @@ const headerSX = { '& .MuiCardHeader-action': { mr: 0 } };
77
export interface MainCardProps extends Omit<CardProps, 'content' | 'title' | 'children' | 'sx'> {
88
border?: boolean;
99
boxShadow?: boolean;
10-
children: React.ReactNode | string;
11-
style?: React.CSSProperties;
10+
children: ReactNode | string;
11+
style?: CSSProperties;
1212
content?: boolean;
1313
className?: string;
1414
contentClass?: string;
1515
contentSX?: CardContentProps['sx'];
1616
darkTitle?: boolean;
1717
sx?: SxProps<Theme>; // you said this is always an object
18-
secondary?: CardHeaderProps['action'] | React.ReactNode;
18+
secondary?: CardHeaderProps['action'] | ReactNode;
1919
shadow?: string;
2020
elevation?: number;
21-
title?: React.ReactNode | string;
21+
title?: ReactNode | string;
2222
}
2323

24-
const MainCard = React.forwardRef<HTMLDivElement, MainCardProps>(function MainCard(
24+
const MainCard = forwardRef<HTMLDivElement, MainCardProps>(function MainCard(
2525
{
2626
border = true,
2727
boxShadow,
@@ -54,9 +54,9 @@ const MainCard = React.forwardRef<HTMLDivElement, MainCardProps>(function MainCa
5454

5555
return (
5656
<Card ref={ref} {...others} sx={mergedSx}>
57-
{!darkTitle && title && <CardHeader sx={headerSX} title={title} action={secondary as React.ReactNode} />}
57+
{!darkTitle && title && <CardHeader sx={headerSX} title={title} action={secondary as ReactNode} />}
5858
{darkTitle && title && (
59-
<CardHeader sx={headerSX} title={<Typography variant="h3">{title}</Typography>} action={secondary as React.ReactNode} />
59+
<CardHeader sx={headerSX} title={<Typography variant="h3">{title}</Typography>} action={secondary as ReactNode} />
6060
)}
6161
{title && <Divider />}
6262
{content ? (

react/src/ui-component/form/Form.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1+
import { useState } from 'react';
2+
import { isAxiosError } from 'axios';
13
import { useNavigate, useParams } from 'react-router-dom';
24
import { useSelector } from 'react-redux';
5+
import { FormattedMessage, useIntl } from 'react-intl';
6+
import { Button } from '@mui/material';
37
import { DefaultRootStateProps, ModelType, create_form_fields, FormField, get_form_by_model, FieldValue, FormModelType } from '@/types';
48
import { array_obj_to_obj_with_key } from '@/utils/transformation';
59
import { createOrUpdateRow } from '@/store/generalSlice';
610
import { useAppDispatch } from '@/store';
7-
import { FormattedMessage, useIntl } from 'react-intl';
811
import DynamicForm from './DynamicForm';
9-
import { Button } from '@mui/material';
1012
import MainCard from '../cards/MainCard';
13+
import FormError from '@/ui-component/form/FormError';
1114

12-
const Form = () => {
15+
export default function Form() {
1316
const intl = useIntl();
14-
1517
const dispatch = useAppDispatch();
1618

1719
const navigate = useNavigate();
1820
const { id, model } = useParams() as {
1921
id: string;
2022
model: ModelType;
2123
};
24+
const [fieldError, setFieldError] = useState<string | null>(null);
25+
2226
const { models, student_id, user_id } = useSelector((state: DefaultRootStateProps) => state.general);
2327

2428
const is_add: boolean = id === 'add';
@@ -33,22 +37,40 @@ const Form = () => {
3337
const data: Record<string, FieldValue> = Object.fromEntries(
3438
send_fields
3539
.filter((f: FormField) => f.key != null)
36-
.map<[string, FieldValue]>((f) => [String(f.key), typeof f.value === 'string' ? f.value.trim() : (f.value ?? null)])
40+
.map<[string, FieldValue]>((f) => {
41+
if (typeof f.value === 'string') {
42+
const trimmed = f.value.trim();
43+
return [String(f.key), trimmed === '' ? null : trimmed];
44+
}
45+
return [String(f.key), f.value ?? null];
46+
})
3747
);
3848
if ([ModelType.student, ModelType.assignment].includes(model)) {
3949
data.teacher_id = user_id;
4050
}
4151
if (model === ModelType.assignment) {
4252
data.student_id = student_id;
4353
}
54+
4455
try {
4556
const message = intl.formatMessage({ id: is_add ? 'toast.create_success' : 'toast.edit_success' }, { model });
4657
await dispatch(createOrUpdateRow({ model, data, id, message })).unwrap();
4758
if (model === ModelType.teacher) navigate(`/login`);
4859
else if (model === ModelType.student && is_add) navigate(`/${ModelType.student}`);
4960
else navigate(`/view/${student_id}`);
50-
} catch (err) {
51-
console.error('Error submitting form:', err);
61+
} catch (err: unknown) {
62+
if (isAxiosError(err)) {
63+
let intlId = err.response?.data?.message ?? 'form.error';
64+
65+
const errors = err.response?.data?.errors;
66+
if (Array.isArray(errors) && errors.length > 0 && errors[0]?.field) {
67+
intlId = `form.label.${errors[0].field}`;
68+
}
69+
70+
setFieldError(intlId);
71+
} else {
72+
setFieldError('form.error');
73+
}
5274
}
5375
};
5476

@@ -58,8 +80,7 @@ const Form = () => {
5880
<FormattedMessage id="form.button.back" />
5981
</Button>
6082
<DynamicForm title={title} fields={fields} onSubmit={handleSubmit} />
83+
{fieldError && <FormError fieldError={fieldError} />}
6184
</MainCard>
6285
);
63-
};
64-
65-
export default Form;
86+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { FormattedMessage, useIntl } from 'react-intl';
2+
import { toast } from 'sonner';
3+
import { Typography } from '@mui/material';
4+
5+
export default function FormError({ fieldError }: { fieldError: string }) {
6+
const intl = useIntl();
7+
const isFieldSpecific = fieldError.startsWith('form.label.');
8+
const isUnique = fieldError.includes('unique:');
9+
10+
let baseFieldId = isUnique
11+
? fieldError.slice(fieldError.indexOf('unique:') + 'unique:'.length)
12+
: fieldError.slice('form.label.'.length);
13+
toast.error(intl.formatMessage({ id: 'form.error' }), {
14+
description: isUnique
15+
? intl.formatMessage({ id: 'form.error.unique' }, { field: intl.formatMessage({ id: baseFieldId }) })
16+
: intl.formatMessage({ id: 'form.error.required.dynamic' }, { field: intl.formatMessage({ id: baseFieldId }) })
17+
});
18+
return (
19+
<Typography textAlign="center" fontSize={isFieldSpecific ? '2em' : '2em'} color="error" mt={2}>
20+
{isUnique ? (
21+
<FormattedMessage id="form.error.unique" values={{ field: intl.formatMessage({ id: baseFieldId }) }} />
22+
) : isFieldSpecific ? (
23+
<FormattedMessage id="form.error.required.dynamic" values={{ field: intl.formatMessage({ id: baseFieldId }) }} />
24+
) : fieldError === 'form.error' ? (
25+
<FormattedMessage id={fieldError} />
26+
) : (
27+
<>{fieldError}</>
28+
)}
29+
</Typography>
30+
);
31+
}

react/src/ui-component/table/MuiTable.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import { fetchRowsByModel } from '@/store/generalSlice';
99
import { useAppDispatch } from '@/store';
1010
import { DefaultRootStateProps, get_columns_mui_by_model, ModelType, MUITableModelType } from '@/types';
1111

12-
const MuiTable = ({ model }: { model: ModelType }) => {
12+
export default function MuiTable({ model }: { model: ModelType }) {
1313
const cells = get_columns_mui_by_model(model);
1414
const dispatch = useAppDispatch();
15+
const rows: MUITableModelType[] = useSelector((state: DefaultRootStateProps) => state.general.models[model] as MUITableModelType[]);
1516

16-
const rows: MUITableModelType[] = Array.isArray(useSelector((state: DefaultRootStateProps) => state.general.models[model]))
17-
? (useSelector((state: DefaultRootStateProps) => state.general.models[model]) as MUITableModelType[])
18-
: [];
1917
useEffect(() => {
2018
dispatch(fetchRowsByModel({ model }));
2119
}, [dispatch, model]);
@@ -58,6 +56,4 @@ const MuiTable = ({ model }: { model: ModelType }) => {
5856
</MainCard>
5957
</Grid>
6058
);
61-
};
62-
63-
export default MuiTable;
59+
}

react/src/ui-component/table/ag-grid/AGTableRenderer.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const defaultColDef: ColDef = {
1212
sortable: true
1313
};
1414

15-
const AGTableRenderer = ({ cols, rows }: { cols: ColDef<AGTableModelType>[]; rows: AGTableModelType[] }) => {
15+
export default function AGTableRenderer({ cols, rows }: { cols: ColDef<AGTableModelType>[]; rows: AGTableModelType[] }) {
1616
const intl = useIntl();
1717
const customization = useSelector((state: DefaultRootStateProps) => state.customization);
1818

@@ -47,6 +47,4 @@ const AGTableRenderer = ({ cols, rows }: { cols: ColDef<AGTableModelType>[]; row
4747
/>
4848
</div>
4949
);
50-
};
51-
52-
export default AGTableRenderer;
50+
}

react/src/utils/locales/en.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"grade": "Grade",
66
"title": "Title",
77
"detail": "Detail",
8+
"form.label.name": "Name",
9+
"form.label.id": "ID",
10+
"form.label.phone": "Phone",
11+
"form.label.grade": "Grade",
12+
"form.label.title": "Title",
13+
"form.label.detail": "Detail",
814
"form_header_add_teacher": "Add Teacher",
915
"form_header_add_student": "Add Student",
1016
"form_header_add_assignment": "Add Assignment",
@@ -24,5 +30,9 @@
2430
"success.delete.student": "Student deleted successfully\nAlso all of their assignments were deleted.",
2531
"success.delete.assignment": "Assignment deleted successfully",
2632
"toast.create_success": "Successfully created {model}",
27-
"toast.edit_success": "Successfully updated {model}"
33+
"toast.edit_success": "Successfully updated {model}",
34+
35+
"form.error": "Save failed",
36+
"form.error.unique": "The field {field} must be unique",
37+
"form.error.required.dynamic": "The field {field} is required"
2838
}

0 commit comments

Comments
 (0)