Skip to content

Commit 83082b1

Browse files
committed
Migrate frontend forms to new form system
1 parent e938fc3 commit 83082b1

18 files changed

+496
-383
lines changed

apps/client/src/components/Entity/EntityDeleteDialog.tsx

+13-10
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { Utils } from '../../utils';
88
import Button from '../Button';
99
import FormError from '../Form/FormError';
1010
import IconButton from '../IconButton';
11-
import FormInput from '../Form/FormInput';
1211
import { useForm } from 'react-hook-form';
12+
import Form from '../Form/Form';
13+
import FormField from '../Form/FormField';
14+
import FormInput from '../Form/FormInput';
1315

1416
export interface EntityDeleteDialogProps {
1517
entityName: string;
@@ -76,16 +78,17 @@ function EntityDeleteDialog({
7678
</ul>
7779

7880
{confirmBeforeDelete && (
79-
<FormInput
80-
label={`Confirm ${identifier} name to delete`}
81-
autoFocus
82-
disabled={loading}
83-
autoComplete="off"
84-
{...register('name')}
85-
/>
86-
)}
81+
<Form loading={loading}>
82+
<FormField label={`Confirm ${identifier} name to delete`}>
83+
<FormInput
84+
autoComplete="off"
85+
{...register('name')}
86+
/>
87+
</FormField>
8788

88-
<FormError>{error}</FormError>
89+
<FormError>{error}</FormError>
90+
</Form>
91+
)}
8992

9093
<div className="mt-6 flex gap-3">
9194
<Button
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createContext } from 'react';
2+
3+
type HTMLFormProps = React.DetailedHTMLProps<
4+
React.FormHTMLAttributes<HTMLFormElement>,
5+
HTMLFormElement
6+
>;
7+
export interface FormProps extends HTMLFormProps {
8+
loading?: boolean;
9+
}
10+
11+
export const FormContext = createContext<{
12+
formLoading: boolean;
13+
}>(null!);
14+
15+
function Form({ loading, ...props }: FormProps) {
16+
return (
17+
<FormContext.Provider value={{ formLoading: loading || false }}>
18+
<form {...props} />
19+
</FormContext.Provider>
20+
);
21+
}
22+
export default Form;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { forwardRef } from 'react';
2+
import FormCurrencySuffix from './FormCurrencySuffix';
3+
import FromInput, { FormInputProps } from './FormInput';
4+
5+
export interface FormCurrencyInput extends FormInputProps {
6+
placeholder: string;
7+
}
8+
9+
const FormCurrencyInput = forwardRef(function FormCurrencyInput(
10+
props: FormCurrencyInput,
11+
ref: any,
12+
) {
13+
return (
14+
<>
15+
<FromInput
16+
{...props}
17+
ref={ref}
18+
placeholder="8.00"
19+
type="number"
20+
min={0}
21+
step={0.01}
22+
/>
23+
<FormCurrencySuffix />
24+
</>
25+
);
26+
});
27+
28+
export default FormCurrencyInput;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useContext } from 'react';
2+
import { CurrentAppContext } from '../Context/CurrentAppContext';
3+
4+
function FormCurrencySuffix() {
5+
const appContext = useContext(CurrentAppContext);
6+
7+
return <span>{appContext.organization.currency}</span>;
8+
}
9+
export default FormCurrencySuffix;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import classNames from 'classnames';
2+
import { FieldError } from 'react-hook-form';
3+
4+
export interface FancyInputProps {
5+
label: string;
6+
hint?: string;
7+
required?: boolean;
8+
className?: string;
9+
children?: React.ReactNode;
10+
errors?: FieldError;
11+
}
12+
13+
function FormField({ label, hint, required, className, errors, children }: FancyInputProps) {
14+
return (
15+
<div className={classNames(className, 'relative my-6')}>
16+
<div className="mb-2 ms-1">
17+
<span className="flex gap-1">
18+
<label>{label}</label>
19+
{hint ? <span className="text-muted">({hint})</span> : null}
20+
{required ? <span>*</span> : null}
21+
</span>
22+
</div>
23+
24+
<div className="flex items-center gap-4">{children}</div>
25+
</div>
26+
);
27+
}
28+
export default FormField;

apps/client/src/components/Form/FormInput.tsx

+16-71
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,24 @@
1-
import classNames from 'classnames';
2-
import React, {
3-
InputHTMLAttributes,
4-
TextareaHTMLAttributes,
5-
forwardRef,
6-
useEffect,
7-
useState,
8-
} from 'react';
1+
import React, { forwardRef, useContext } from 'react';
2+
import { FormContext } from './Form';
3+
import { REGULAR_INPUT_CLASSNAMES } from './regularInputClassnames';
94

10-
type FormControlProps<T extends 'input' | 'textarea'> = T extends 'input'
11-
? InputHTMLAttributes<HTMLInputElement>
12-
: T extends 'textarea'
13-
? TextareaHTMLAttributes<HTMLTextAreaElement>
14-
: never;
5+
type InputProps = React.DetailedHTMLProps<
6+
React.InputHTMLAttributes<HTMLInputElement>,
7+
HTMLInputElement
8+
>;
159

16-
interface FormInputBaseProps<T extends 'input' | 'textarea'> {
17-
label?: string;
18-
hint?: string;
19-
suffixText?: string;
20-
noEndMargin?: boolean;
21-
as?: T;
22-
}
10+
export interface FormInputProps extends InputProps {}
2311

24-
type InputProps = FormInputBaseProps<'input'> & FormControlProps<'input'>;
25-
type TextareaProps = FormInputBaseProps<'textarea'> & FormControlProps<'textarea'>;
26-
type FormInputProps = InputProps | TextareaProps;
27-
28-
const FormInput = forwardRef(function TextInput(
29-
{ label, hint, suffixText, as, noEndMargin, ...props }: FormInputProps,
30-
ref: any,
31-
) {
32-
const [focused, setFocused] = useState(false);
33-
34-
if (props.readOnly) {
35-
props.disabled = true;
36-
}
37-
38-
useEffect(() => {
39-
if (props.disabled) {
40-
setFocused(false);
41-
}
42-
}, [props.disabled]);
43-
44-
const element = React.createElement(as || 'input', {
45-
...props,
46-
ref,
47-
onFocus: () => setFocused(true),
48-
onBlur: () => setFocused(false),
49-
className: classNames(
50-
'flex flex-1 bg-inherit px-4 py-2 outline-none disabled:bg-gray-100 ',
51-
'rounded-md text-muted',
52-
),
53-
});
12+
const FormInput = forwardRef(function FromInput(props: FormInputProps, ref: any) {
13+
const { formLoading: formDisabled } = useContext(FormContext);
5414

5515
return (
56-
<div className={classNames('relative', !noEndMargin && 'my-6')}>
57-
<div className="mb-2 ms-1">
58-
<label className="flex gap-1">
59-
{label}
60-
{hint ? <span className="text-muted">({hint})</span> : null}
61-
{props.required ? <span>*</span> : null}
62-
</label>
63-
</div>
64-
65-
<div className="flex items-center gap-4">
66-
<div
67-
className={classNames(
68-
'flex flex-1 rounded-md border transition-colors',
69-
focused ? 'border-primary' : 'border-gray-300',
70-
)}
71-
>
72-
{element}
73-
</div>
74-
{suffixText ? <span>{suffixText}</span> : null}
75-
</div>
76-
</div>
16+
<input
17+
{...props}
18+
ref={ref}
19+
disabled={formDisabled || props.readOnly || props.disabled}
20+
className={REGULAR_INPUT_CLASSNAMES}
21+
/>
7722
);
7823
});
7924

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useContext } from 'react';
2+
import Button, { ButtonProps } from '../Button';
3+
import { FormContext } from './Form';
4+
5+
export interface FormSubmitButtonProps extends ButtonProps {}
6+
7+
function FormSubmitButton(props: FormSubmitButtonProps) {
8+
const { formLoading } = useContext(FormContext);
9+
10+
return (
11+
<Button
12+
{...props}
13+
role="submit"
14+
className="mt-4"
15+
loading={formLoading}
16+
/>
17+
);
18+
}
19+
export default FormSubmitButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React, { forwardRef, useContext } from 'react';
2+
import { FormContext } from './Form';
3+
import { REGULAR_INPUT_CLASSNAMES } from './regularInputClassnames';
4+
5+
type TextareaProps = React.DetailedHTMLProps<
6+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
7+
HTMLTextAreaElement
8+
>;
9+
10+
export interface FormTextAreaProps extends TextareaProps {}
11+
12+
const FormTextArea = forwardRef(function FormTextArea(props: FormTextAreaProps, ref: any) {
13+
const { formLoading: formDisabled } = useContext(FormContext);
14+
15+
return (
16+
<textarea
17+
{...props}
18+
ref={ref}
19+
rows={props.rows ?? 3}
20+
disabled={formDisabled || props.readOnly || props.disabled}
21+
className={REGULAR_INPUT_CLASSNAMES}
22+
/>
23+
);
24+
});
25+
26+
export default FormTextArea;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import classNames from 'classnames';
2+
3+
const REGULAR_INPUT_CLASSNAMES = classNames(
4+
'flex flex-1 bg-inherit px-4 py-2 outline-none disabled:bg-gray-100',
5+
'text-muted',
6+
'rounded-md border border-gray-300 focus:border-primary ',
7+
);
8+
9+
export { REGULAR_INPUT_CLASSNAMES };

apps/client/src/components/IconButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface IconButtonProps extends ButtonProps {
99
}
1010

1111
function IconButton({ icon, ...props }: IconButtonProps) {
12-
const iconElement = React.createElement(icon || BsPlusCircle, { size: 20 });
12+
const iconElement = icon ? React.createElement(icon, { size: 20 }) : null;
1313

1414
return (
1515
<Button

apps/client/src/components/InventoryItem/InventoryAddForm.tsx

+39-34
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
77
import { CreateInventoryItemDto, OrganizationDto } from 'shared-types';
88
import useProductsDetails from '../../hooks/useProductsDetails';
99
import { Utils } from '../../utils';
10-
import Button from '../Button';
1110
import { CurrentAppContext } from '../Context/CurrentAppContext';
11+
import Form from '../Form/Form';
1212
import FormError from '../Form/FormError';
13+
import FormField from '../Form/FormField';
1314
import FormInput from '../Form/FormInput';
15+
import FormSubmitButton from '../Form/FormSubmitButton';
1416

1517
type Inputs = {
1618
quantity: number;
@@ -53,23 +55,27 @@ function InventoryAddForm() {
5355
}
5456

5557
return (
56-
<form onSubmit={handleSubmit(onSubmit)}>
57-
<FormInput
58-
label="Warehouse"
59-
readOnly
60-
required
61-
value={appContext.currentWarehouse.name}
62-
/>
58+
<Form
59+
onSubmit={handleSubmit(onSubmit)}
60+
loading={loading}
61+
>
62+
<FormField label="Warehouse">
63+
<FormInput
64+
readOnly
65+
required
66+
value={appContext.currentWarehouse.name}
67+
/>
68+
</FormField>
6369

64-
<FormInput
70+
<FormField
6571
label="Product"
66-
disabled
67-
value={product?.name}
68-
noEndMargin
69-
minLength={2}
70-
maxLength={32}
71-
required
72-
/>
72+
className="mb-0"
73+
>
74+
<FormInput
75+
disabled
76+
value={product?.name}
77+
/>
78+
</FormField>
7379
<Link
7480
className={classNames('link-primary mb-1 ms-1 mt-3 flex items-center gap-2', {
7581
'animate-bounce': product == undefined,
@@ -82,32 +88,31 @@ function InventoryAddForm() {
8288
<BsArrowLeftRight /> {product ? 'Change' : 'Select'} product
8389
</Link>
8490

85-
<FormInput
91+
<FormField
8692
label="Quantity"
8793
hint="If you know current item quantity you can add it here"
88-
placeholder="0"
89-
type="number"
90-
{...register('quantity', { setValueAs: (v) => (v == null ? 0 : +v) })}
91-
/>
94+
>
95+
<FormInput
96+
placeholder="0"
97+
type="number"
98+
{...register('quantity', { setValueAs: (v) => (v == null ? 0 : +v) })}
99+
/>
100+
</FormField>
92101

93-
<FormInput
102+
<FormField
94103
label="Location"
95104
hint="Where is this item located in warehouse"
96-
placeholder="shelf / aisle / bin number"
97-
{...register('location')}
98-
/>
105+
>
106+
<FormInput
107+
placeholder="shelf / aisle / bin number"
108+
{...register('location')}
109+
/>
110+
</FormField>
99111

100112
<FormError>{error}</FormError>
101113

102-
<Button
103-
role="submit"
104-
className="mt-4"
105-
loading={loading}
106-
disabled={product == undefined}
107-
>
108-
Add product to inventory
109-
</Button>
110-
</form>
114+
<FormSubmitButton disabled={product == undefined}>Add product to inventory</FormSubmitButton>
115+
</Form>
111116
);
112117
}
113118
export default InventoryAddForm;

0 commit comments

Comments
 (0)