You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
7.8 KiB
252 lines
7.8 KiB
import {
|
|
Controller,
|
|
DeepPartial,
|
|
FieldValues,
|
|
Path,
|
|
SubmitHandler,
|
|
useForm
|
|
} from "react-hook-form";
|
|
import { Input } from "./UI/Input.js";
|
|
import { Label } from "./UI/Label.js";
|
|
import { ErrorMessage } from "@hookform/error-message";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue
|
|
} from "./UI/Select.js";
|
|
import { Switch } from "./UI/Switch.js";
|
|
import { H4 } from "./UI/Typography/H4.js";
|
|
import { Subtle } from "./UI/Typography/Subtle.js";
|
|
|
|
interface DisabledBy<T> {
|
|
fieldName: Path<T>;
|
|
selector?: number;
|
|
invert?: boolean;
|
|
}
|
|
|
|
interface BasicFieldProps<T> {
|
|
name: Path<T>;
|
|
label: string;
|
|
description?: string;
|
|
active?: boolean;
|
|
required?: boolean;
|
|
disabledBy?: DisabledBy<T>[];
|
|
}
|
|
|
|
interface InputFieldProps<T> extends BasicFieldProps<T> {
|
|
type: "text" | "number" | "password";
|
|
suffix?: string;
|
|
}
|
|
|
|
interface SelectFieldProps<T> extends BasicFieldProps<T> {
|
|
type: "select" | "multiSelect";
|
|
|
|
enumValue: {
|
|
[s: string]: string | number;
|
|
};
|
|
formatEnumName?: boolean;
|
|
}
|
|
|
|
interface ToggleFieldProps<T> extends BasicFieldProps<T> {
|
|
type: "toggle";
|
|
}
|
|
|
|
export interface FormProps<T extends FieldValues> {
|
|
onSubmit: SubmitHandler<T>;
|
|
defaultValues?: DeepPartial<T>;
|
|
fieldGroups: {
|
|
label: string;
|
|
description: string;
|
|
fields: (InputFieldProps<T> | SelectFieldProps<T> | ToggleFieldProps<T>)[];
|
|
}[];
|
|
}
|
|
|
|
export function DynamicForm<T extends FieldValues>({
|
|
fieldGroups,
|
|
onSubmit,
|
|
defaultValues
|
|
}: FormProps<T>) {
|
|
const { register, handleSubmit, control, getValues } = useForm<T>({
|
|
mode: "onChange",
|
|
defaultValues: defaultValues
|
|
});
|
|
|
|
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => {
|
|
if (!disabledBy) return false;
|
|
|
|
return disabledBy.some((field) => {
|
|
const value = getValues(field.fieldName);
|
|
if (typeof value === "boolean") return field.invert ? value : !value;
|
|
if (typeof value === "number")
|
|
return field.invert
|
|
? field.selector !== value
|
|
: field.selector === value;
|
|
return false;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<form
|
|
className="space-y-8 divide-y divide-gray-200"
|
|
onChange={handleSubmit(onSubmit)}
|
|
>
|
|
{fieldGroups.map((fieldGroup, index) => (
|
|
<div
|
|
key={index}
|
|
className="space-y-8 divide-y divide-gray-200 sm:space-y-5"
|
|
>
|
|
<div>
|
|
<H4 className="font-medium">{fieldGroup.label}</H4>
|
|
<Subtle>{fieldGroup.description}</Subtle>
|
|
</div>
|
|
|
|
{fieldGroup.fields.map((field, index) => {
|
|
const fieldWrapperData: FieldWrapperProps = {
|
|
label: field.label,
|
|
description: field.description,
|
|
disabled: isDisabled(field.disabledBy)
|
|
};
|
|
|
|
switch (field.type) {
|
|
case "text":
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
<Input
|
|
disabled={fieldWrapperData.disabled}
|
|
{...register(field.name)}
|
|
/>
|
|
</FieldWrapper>
|
|
);
|
|
case "number":
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
<Controller
|
|
name={field.name}
|
|
control={control}
|
|
render={({ field: { value, onChange, ...rest } }) => (
|
|
<Input
|
|
type="number"
|
|
value={parseInt(value)}
|
|
onChange={(e) => onChange(parseInt(e.target.value))}
|
|
disabled={fieldWrapperData.disabled}
|
|
/>
|
|
)}
|
|
/>
|
|
</FieldWrapper>
|
|
);
|
|
case "password":
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
<Input
|
|
type="password"
|
|
disabled={fieldWrapperData.disabled}
|
|
{...register(field.name)}
|
|
/>
|
|
</FieldWrapper>
|
|
);
|
|
case "toggle":
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
<Controller
|
|
name={field.name}
|
|
control={control}
|
|
render={({ field: { value, onChange, ...rest } }) => (
|
|
<Switch
|
|
checked={value}
|
|
onCheckedChange={onChange}
|
|
disabled={fieldWrapperData.disabled}
|
|
{...rest}
|
|
/>
|
|
)}
|
|
/>
|
|
</FieldWrapper>
|
|
);
|
|
case "select":
|
|
const optionsEnumValues = field.enumValue
|
|
? Object.entries(field.enumValue).filter(
|
|
(value) => typeof value[1] === "number"
|
|
)
|
|
: [];
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
<Controller
|
|
name={field.name}
|
|
control={control}
|
|
render={({ field: { value, onChange, ...rest } }) => (
|
|
<Select
|
|
onValueChange={(e) => onChange(parseInt(e))}
|
|
disabled={fieldWrapperData.disabled}
|
|
value={value?.toString()}
|
|
{...rest}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{optionsEnumValues.map(([name, value], index) => (
|
|
<SelectItem key={index} value={value.toString()}>
|
|
{field.formatEnumName
|
|
? name
|
|
.replace(/_/g, " ")
|
|
.toLowerCase()
|
|
.split(" ")
|
|
.map(
|
|
(s) =>
|
|
s.charAt(0).toUpperCase() +
|
|
s.substring(1)
|
|
)
|
|
.join(" ")
|
|
: name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
/>
|
|
</FieldWrapper>
|
|
);
|
|
case "multiSelect":
|
|
return (
|
|
<FieldWrapper key={index} {...fieldWrapperData}>
|
|
tmp
|
|
</FieldWrapper>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
))}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
interface FieldWrapperProps {
|
|
label: string;
|
|
description?: string;
|
|
disabled?: boolean;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const FieldWrapper = ({
|
|
label,
|
|
description,
|
|
disabled,
|
|
children
|
|
}: FieldWrapperProps): JSX.Element => (
|
|
<div className="pt-6 sm:pt-5">
|
|
<div role="group" aria-labelledby="label-notifications">
|
|
<div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4">
|
|
<Label>{label}</Label>
|
|
<div className="sm:col-span-2">
|
|
<div className="max-w-lg">
|
|
<p className="text-sm text-gray-500">{description}</p>
|
|
<div className="mt-4 space-y-4">
|
|
<div className="flex items-center">{children}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|