Skip to content

Commit 35f8d32

Browse files
authored
[OGUI-1831] Introduce react hook form library (#3210)
Introduced react-hook-form library for better form state management and validation.
1 parent 47d9985 commit 35f8d32

File tree

13 files changed

+404
-19
lines changed

13 files changed

+404
-19
lines changed

Configuration/webapp/app/api/axiosInstance.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const axiosInstance = axios.create({
2121
baseURL: API_URL,
2222
headers: {
2323
'Content-Type': 'application/json',
24-
'User-Agent': 'axios 0.21.1',
2524
},
2625
withCredentials: false,
2726
});

Configuration/webapp/app/components/form/AccordionHeader.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ interface AccordionHeaderProps extends PropsWithChildren {
2525
viewFormToggle: () => void;
2626
}
2727

28+
/**
29+
* Accordion header component.
30+
* @param {AccordionHeaderProps} props - The props of the accordion header.
31+
* @param {string} props.title - The title of the accordion header.
32+
* @param {boolean} props.viewForm - Whether the form is visible.
33+
* @param {() => void} props.viewFormToggle - The callback to toggle the form visibility.
34+
* @returns {ReactElement} The accordion header component.
35+
*/
2836
export const AccordionHeader: FC<AccordionHeaderProps> = ({
2937
title,
3038
viewForm,

Configuration/webapp/app/components/form/Form.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import Stack from '@mui/material/Stack';
1919
import { Widget } from './Widget';
2020
import { AccordionHeader } from './AccordionHeader';
2121
import { Typography } from '@mui/material';
22+
import type { Control } from 'react-hook-form';
23+
import { type InputsType } from '~/routes/configuration';
24+
import { KEY_SEPARATOR } from './constants';
2225

2326
export type FormItem = { [key: string]: string | object | FormItem };
2427

@@ -28,8 +31,10 @@ export type FormRestrictions = {
2831

2932
interface FormProps extends PropsWithChildren {
3033
sectionTitle: string;
34+
sectionPrefix: string;
3135
items: FormItem;
3236
itemsRestrictions: FormRestrictions;
37+
control: Control<InputsType>;
3338
}
3439

3540
/**
@@ -45,7 +50,23 @@ function isFormRestrictions(obj: FormRestrictions[string]): obj is FormRestricti
4550
return obj instanceof Object && !(obj instanceof Array);
4651
}
4752

48-
export const Form: FC<FormProps> = ({ sectionTitle, items, itemsRestrictions }) => {
53+
/**
54+
* Form component.
55+
* @param {FormProps} props - The props of the form.
56+
* @param {string} props.sectionTitle - The title of the section.
57+
* @param {string} props.sectionPrefix - The prefix of the section.
58+
* @param {FormItem} props.items - The items of the form.
59+
* @param {FormRestrictions} props.itemsRestrictions - The restrictions of the items.
60+
* @param {Control<InputsType>} props.control - The control of the form.
61+
* @returns {ReactElement} The form component.
62+
*/
63+
export const Form: FC<FormProps> = ({
64+
sectionTitle,
65+
sectionPrefix,
66+
items,
67+
itemsRestrictions,
68+
control,
69+
}) => {
4970
const [viewForm, setViewForm] = useState<boolean>(true);
5071

5172
const renderItem = useCallback(
@@ -54,11 +75,20 @@ export const Form: FC<FormProps> = ({ sectionTitle, items, itemsRestrictions })
5475
<Form
5576
key={key}
5677
sectionTitle={key}
78+
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${key}`}
5779
items={items[key] as FormItem}
5880
itemsRestrictions={itemsRestrictions[key] as FormRestrictions}
81+
control={control}
5982
/>
6083
) : (
61-
<Widget key={key} title={key} type={value} value={items[key]} />
84+
<Widget
85+
key={key}
86+
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${key}`}
87+
label={key}
88+
type={value}
89+
value={items[key]}
90+
control={control}
91+
/>
6292
),
6393
[items, itemsRestrictions],
6494
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import { Fab, Typography } from '@mui/material';
16+
17+
interface SaveButtonProps {
18+
onClick: () => void;
19+
disabled: boolean;
20+
}
21+
22+
const fabStyle = {
23+
position: 'absolute',
24+
bottom: 16,
25+
right: 16,
26+
padding: 5,
27+
};
28+
29+
/**
30+
* Save button component.
31+
* @param {SaveButtonProps} props - The props of the save button.
32+
* @param {() => void} props.onClick - The callback to click the save button.
33+
* @param {boolean} props.disabled - Whether the save button is disabled.
34+
* @returns {ReactElement} The save button component.
35+
*/
36+
export const SaveButton = ({ onClick, disabled }: SaveButtonProps) => (
37+
<Fab
38+
color="primary"
39+
aria-label="add"
40+
onClick={onClick}
41+
disabled={disabled}
42+
sx={fabStyle}
43+
variant="extended"
44+
>
45+
<Typography variant="h6">SAVE</Typography>
46+
</Fab>
47+
);

Configuration/webapp/app/components/form/Widget.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,39 @@
1313
*/
1414

1515
import { type FC, type PropsWithChildren, type ReactElement } from 'react';
16-
import FormControlLabel from '@mui/material/FormControlLabel';
17-
import TextField from '@mui/material/TextField';
18-
import Switch from '@mui/material/Switch';
16+
import { useFormState, type Control } from 'react-hook-form';
17+
import type { InputsType } from '~/routes/configuration';
18+
import { FormTextInput } from './widgets/FormTextInput';
19+
import { FormNumberInput } from './widgets/FormNumberInput';
20+
import { FormToggleInput } from './widgets/FormToggleInput';
1921

20-
interface WidgetProps extends PropsWithChildren {
21-
title: string;
22+
export interface WidgetProps extends PropsWithChildren {
23+
sectionPrefix: string;
24+
label: string;
2225
type: 'string' | 'number' | 'boolean' | 'array';
2326
value: unknown;
27+
control: Control<InputsType>;
2428
}
2529

26-
export const Widget: FC<WidgetProps> = ({ title, type, value }): ReactElement => {
30+
/**
31+
* Widget component.
32+
* @param {WidgetProps} props - The props of the widget.
33+
* @param {string} props.type - The type of the widget.
34+
* @param {string} props.label - The label of the widget.
35+
* @param {unknown} props.value - The value of the widget.
36+
* @param {Control<InputsType>} props.control - The control of the widget.
37+
* @returns {ReactElement} The widget component.
38+
*/
39+
export const Widget: FC<WidgetProps> = ({ type, ...rest }): ReactElement => {
40+
const { dirtyFields } = useFormState({ control: rest.control });
41+
const isDirty = dirtyFields[rest.sectionPrefix];
2742
switch (type) {
2843
case 'string':
29-
return <TextField type="text" defaultValue={value} label={title} />;
44+
return <FormTextInput {...rest} isDirty={Boolean(isDirty)} />;
3045
case 'number':
31-
return <TextField type="number" defaultValue={value} label={title} />;
46+
return <FormNumberInput {...rest} isDirty={Boolean(isDirty)} />;
3247
case 'boolean':
33-
return (
34-
<FormControlLabel control={<Switch defaultChecked={value === 'true'} />} label={title} />
35-
);
48+
return <FormToggleInput {...rest} isDirty={Boolean(isDirty)} />;
3649
case 'array':
3750
return <>array not implemented</>; // TODO OGUI-1803: add implementation after the decision is made
3851
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
export const KEY_SEPARATOR = '__';
16+
export const DEFAULT_PREFIX = 'Configuration';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import { DEFAULT_PREFIX, KEY_SEPARATOR } from '../constants';
16+
import type { FormItem } from '../Form';
17+
18+
/**
19+
* Get the default values from the configuration object.
20+
* @param {FormItem | undefined} obj - The configuration object.
21+
* @param {string} prefix - The prefix of the configuration object.
22+
* @returns {Record<string, string | number | boolean>} The default values.
23+
*/
24+
export const getDefaultValuesFromConfigObject = (
25+
obj: FormItem | undefined,
26+
prefix: string = DEFAULT_PREFIX,
27+
) => {
28+
if (!obj) {
29+
return {};
30+
}
31+
// omit arrays for now
32+
if (Array.isArray(obj)) {
33+
return {};
34+
}
35+
let result: Record<string, string | number | boolean> = {};
36+
const entries = Object.entries(obj);
37+
for (const [key, value] of entries) {
38+
const newPrefix = `${prefix}${KEY_SEPARATOR}${key}`;
39+
if (typeof value === 'object') {
40+
result = { ...result, ...getDefaultValuesFromConfigObject(value as FormItem, newPrefix) };
41+
} else {
42+
if (value === 'true') {
43+
result[newPrefix] = true;
44+
} else if (value === 'false') {
45+
result[newPrefix] = false;
46+
} else {
47+
result[newPrefix] = value;
48+
}
49+
}
50+
}
51+
return result;
52+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
interface FormNumberInputProps extends Omit<WidgetProps, 'type' | 'value'> {
16+
isDirty: boolean;
17+
}
18+
19+
import { Controller } from 'react-hook-form';
20+
import type { WidgetProps } from '../Widget';
21+
import { TextField } from '@mui/material';
22+
import type { ReactElement } from 'react';
23+
24+
/**
25+
* Number input widget for the form.
26+
* @param {FormNumberInputProps} props - The props of the widget.
27+
* @param {string} props.sectionPrefix - The prefix of the section.
28+
* @param {string} props.label - The label of the widget.
29+
* @param {Control<InputsType>} props.control - The control of the widget.
30+
* @param {boolean} props.isDirty - Whether the widget is dirty.
31+
* @returns {ReactElement} The number input widget.
32+
*/
33+
export const FormNumberInput = ({
34+
sectionPrefix,
35+
label,
36+
control,
37+
isDirty,
38+
}: FormNumberInputProps): ReactElement => (
39+
<Controller
40+
name={sectionPrefix}
41+
control={control}
42+
render={({ field, fieldState: { error } }) => (
43+
<TextField
44+
type="number"
45+
label={label}
46+
{...field}
47+
error={Boolean(error)}
48+
color={isDirty ? 'secondary' : 'primary'}
49+
focused={isDirty}
50+
helperText={error?.message}
51+
/>
52+
)}
53+
/>
54+
);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import { TextField } from '@mui/material';
16+
import { type ReactElement } from 'react';
17+
import { Controller } from 'react-hook-form';
18+
import type { WidgetProps } from '../Widget';
19+
20+
interface FormTextInputProps extends Omit<WidgetProps, 'type' | 'value'> {
21+
isDirty: boolean;
22+
}
23+
24+
/**
25+
* Text input widget for the form.
26+
* @param {FormTextInputProps} props - The props of the widget.
27+
* @param {string} props.sectionTitle - The section title of the widget.
28+
* @param {string} props.label - The title of the widget.
29+
* @param {Control<InputsType>} props.control - The control of the widget.
30+
* @returns {ReactElement} The text input widget.
31+
*/
32+
export const FormTextInput = ({
33+
sectionPrefix,
34+
label,
35+
control,
36+
isDirty,
37+
}: FormTextInputProps): ReactElement => (
38+
<Controller
39+
name={sectionPrefix}
40+
control={control}
41+
render={({ field, fieldState: { error } }) => (
42+
<TextField
43+
type="text"
44+
label={label}
45+
{...field}
46+
error={Boolean(error)}
47+
color={isDirty ? 'secondary' : 'primary'}
48+
focused={isDirty}
49+
helperText={error?.message}
50+
/>
51+
)}
52+
/>
53+
);

0 commit comments

Comments
 (0)