feat(forms): implement dynamic form builder for admin and company creation
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced DynamicFormBuilder component to streamline form creation for admin and company categories. - Refactored CreateAdmin and CreateCompany components to utilize the new dynamic form structure. - Added createAdminFormSections and createCompanyFormSections functions to define form fields and validation rules. - Enhanced form handling with improved error management and dynamic field rendering. This update enhances maintainability and scalability of forms across the application.
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
import { GlobeIcon, MoneyIcon } from "@/assets/icons";
|
||||
import TagInput from "@/components/FormElements/InputGroup/tag-input";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import type { DynamicFormSection } from "@/components/forms/DynamicFormBuilder";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import type { ICreateCompanyCredentials } from "@/lib/api";
|
||||
import type { ILabelValue } from "@/types/shared";
|
||||
import type { UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
import CompanyDocuments from "./documents/CompanyDocuments";
|
||||
|
||||
interface CompanyFormSectionsOptions {
|
||||
editMode?: boolean;
|
||||
id?: string;
|
||||
categoryItems: ILabelValue[];
|
||||
watch: UseFormWatch<ICreateCompanyCredentials>;
|
||||
setValue: UseFormSetValue<ICreateCompanyCredentials>;
|
||||
}
|
||||
|
||||
const optionalNumber = (value: unknown) => {
|
||||
if (value === "" || value === null || value === undefined) return undefined;
|
||||
return Number(value);
|
||||
};
|
||||
|
||||
const optionalNumberRange = ({
|
||||
min,
|
||||
max,
|
||||
integer = false,
|
||||
}: {
|
||||
min: number;
|
||||
max: number;
|
||||
integer?: boolean;
|
||||
}) => {
|
||||
return (value: unknown) => {
|
||||
const numberValue = optionalNumber(value);
|
||||
|
||||
if (numberValue === undefined) return true;
|
||||
if (!Number.isFinite(numberValue)) return "Enter a valid number";
|
||||
if (integer && !Number.isInteger(numberValue)) {
|
||||
return "This field must be a whole number";
|
||||
}
|
||||
if (numberValue < min) return FormErrorMessages.min(min);
|
||||
if (numberValue > max) return FormErrorMessages.max(max);
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const languageItems: ILabelValue[] = [
|
||||
{ value: "en", label: "EN" },
|
||||
{ value: "ar", label: "AR" },
|
||||
{ value: "fr", label: "FR" },
|
||||
{ value: "es", label: "ES" },
|
||||
{ value: "de", label: "DE" },
|
||||
{ value: "zh", label: "ZH" },
|
||||
{ value: "jEna", label: "jEna" },
|
||||
{ value: "ko", label: "KO" },
|
||||
];
|
||||
|
||||
const currencyItems: ILabelValue[] = [
|
||||
{ label: "USD", value: "USD" },
|
||||
{ label: "EUR", value: "EUR" },
|
||||
{ label: "GBP", value: "GBP" },
|
||||
{ label: "JPY", value: "JPY" },
|
||||
{ label: "CAD", value: "CAD" },
|
||||
{ label: "AUD", value: "AUD" },
|
||||
{ label: "CHF", value: "CHF" },
|
||||
{ label: "CNY", value: "CNY" },
|
||||
{ label: "INR", value: "INR" },
|
||||
{ label: "BRL", value: "BRL" },
|
||||
];
|
||||
|
||||
export const createCompanyFormSections = ({
|
||||
editMode,
|
||||
id,
|
||||
categoryItems,
|
||||
watch,
|
||||
setValue,
|
||||
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
|
||||
{
|
||||
title: "Company information",
|
||||
rootClassName: "lg:col-span-2",
|
||||
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
|
||||
fields: [
|
||||
{
|
||||
kind: "input",
|
||||
name: "name",
|
||||
label: "Name",
|
||||
type: "text",
|
||||
placeholder: "Legal name as registered",
|
||||
required: true,
|
||||
className: "sm:col-span-2",
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 200, message: FormErrorMessages.maxLength(200) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "email",
|
||||
label: "Email",
|
||||
type: "email",
|
||||
placeholder: "name@company.com",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: FormErrorMessages.invalidEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "phone",
|
||||
label: "Phone",
|
||||
type: "tel",
|
||||
placeholder: "+971 50 123 4567",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 10, message: FormErrorMessages.minLength(10) },
|
||||
maxLength: { value: 20, message: FormErrorMessages.maxLength(20) },
|
||||
pattern: {
|
||||
value: REGEX.PHONE,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "type",
|
||||
render: ({ register, errors }) => (
|
||||
<Select<ICreateCompanyCredentials>
|
||||
{...register("type", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
errors={errors}
|
||||
name="type"
|
||||
label="Company type"
|
||||
required
|
||||
items={[
|
||||
{ value: "private", label: "Private" },
|
||||
{ value: "public", label: "Public" },
|
||||
{ value: "government", label: "Government" },
|
||||
{ value: "ngo", label: "NGO" },
|
||||
{ value: "startup", label: "Startup" },
|
||||
]}
|
||||
defaultValue="Private"
|
||||
prefixIcon={<GlobeIcon />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "industry",
|
||||
label: "Industry",
|
||||
type: "text",
|
||||
placeholder: "e.g. construction, software, logistics",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 100, message: FormErrorMessages.maxLength(100) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "registration_number",
|
||||
label: "Registration number",
|
||||
type: "text",
|
||||
placeholder: "Registry or license number",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 5, message: FormErrorMessages.minLength(5) },
|
||||
maxLength: { value: 50, message: FormErrorMessages.maxLength(50) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "tax_id",
|
||||
label: "Tax id",
|
||||
type: "text",
|
||||
placeholder: "National tax identification number",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 5, message: FormErrorMessages.minLength(5) },
|
||||
maxLength: { value: 50, message: FormErrorMessages.maxLength(50) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "website",
|
||||
label: "Website",
|
||||
type: "text",
|
||||
placeholder: "https://www.example.com",
|
||||
required: true,
|
||||
className: "sm:col-span-2",
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 100, message: FormErrorMessages.maxLength(100) },
|
||||
pattern: {
|
||||
value: REGEX.URL,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "description",
|
||||
className: "sm:col-span-2",
|
||||
render: ({ register, errors }) => (
|
||||
<TextAreaGroup<ICreateCompanyCredentials>
|
||||
label="Description"
|
||||
{...register("description", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 1000,
|
||||
message: FormErrorMessages.maxLength(1000),
|
||||
},
|
||||
})}
|
||||
errors={errors}
|
||||
name="description"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Business information",
|
||||
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
|
||||
fields: [
|
||||
{
|
||||
kind: "input",
|
||||
name: "employee_count",
|
||||
label: "Employee count",
|
||||
type: "number",
|
||||
rules: {
|
||||
setValueAs: optionalNumber,
|
||||
validate: optionalNumberRange({ min: 1, max: 100000, integer: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "annual_revenue",
|
||||
label: "Annual revenue",
|
||||
type: "number",
|
||||
rules: {
|
||||
setValueAs: optionalNumber,
|
||||
validate: optionalNumberRange({ min: 0, max: 999999999999 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "founded_year",
|
||||
label: "Founded year",
|
||||
type: "number",
|
||||
className: "sm:col-span-2",
|
||||
rules: {
|
||||
setValueAs: optionalNumber,
|
||||
validate: optionalNumberRange({
|
||||
min: 1800,
|
||||
max: 2100,
|
||||
integer: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
|
||||
fields: [
|
||||
{
|
||||
kind: "custom",
|
||||
name: "language",
|
||||
render: ({ register, errors }) => (
|
||||
<Select<ICreateCompanyCredentials>
|
||||
{...register("language")}
|
||||
errors={errors}
|
||||
name="language"
|
||||
label="Language"
|
||||
items={languageItems}
|
||||
defaultValue="en"
|
||||
prefixIcon={<GlobeIcon />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "currency",
|
||||
render: ({ register, errors }) => (
|
||||
<Select<ICreateCompanyCredentials>
|
||||
{...register("currency")}
|
||||
errors={errors}
|
||||
name="currency"
|
||||
label="Currency"
|
||||
items={currencyItems}
|
||||
defaultValue="en"
|
||||
prefixIcon={<MoneyIcon />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "timezone",
|
||||
label: "Timezone",
|
||||
type: "text",
|
||||
className: "sm:col-span-2",
|
||||
rules: {
|
||||
minLength: { value: 3, message: FormErrorMessages.minLength(3) },
|
||||
maxLength: { value: 50, message: FormErrorMessages.maxLength(50) },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Address",
|
||||
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
|
||||
fields: [
|
||||
{
|
||||
kind: "input",
|
||||
name: "address.street",
|
||||
label: "Street",
|
||||
type: "text",
|
||||
placeholder: "Building, street, suite or unit",
|
||||
required: true,
|
||||
className: "sm:col-span-2",
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 5, message: FormErrorMessages.minLength(5) },
|
||||
maxLength: { value: 200, message: FormErrorMessages.maxLength(200) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "address.city",
|
||||
label: "City",
|
||||
type: "text",
|
||||
placeholder: "City or town",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 100, message: FormErrorMessages.maxLength(100) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "address.state",
|
||||
label: "State",
|
||||
type: "text",
|
||||
placeholder: "State, province, or region",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 100, message: FormErrorMessages.maxLength(100) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "address.postal_code",
|
||||
label: "Postal code",
|
||||
type: "text",
|
||||
placeholder: "Postal or ZIP code",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 3, message: FormErrorMessages.minLength(3) },
|
||||
maxLength: { value: 20, message: FormErrorMessages.maxLength(20) },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "input",
|
||||
name: "address.country",
|
||||
label: "Country",
|
||||
type: "text",
|
||||
placeholder: "Full country name",
|
||||
required: true,
|
||||
rules: {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: { value: 100, message: FormErrorMessages.maxLength(100) },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tags",
|
||||
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
|
||||
fields: [
|
||||
{
|
||||
kind: "custom",
|
||||
name: "tags.keywords",
|
||||
render: ({ register }) => (
|
||||
<TagInput<ICreateCompanyCredentials>
|
||||
{...register("tags.keywords")}
|
||||
name="tags.keywords"
|
||||
label="Keywords"
|
||||
placeholder=""
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
watch={watch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "tags.categories",
|
||||
render: ({ register }) => (
|
||||
<MultiSelect<ICreateCompanyCredentials>
|
||||
{...register("tags.categories")}
|
||||
value={watch("tags.categories")}
|
||||
name="tags.categories"
|
||||
label="Categories"
|
||||
items={categoryItems}
|
||||
placeholder=""
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "tags.cpv_codes",
|
||||
render: ({ register }) => (
|
||||
<TagInput<ICreateCompanyCredentials>
|
||||
{...register("tags.cpv_codes")}
|
||||
label="CPV codes"
|
||||
name="tags.cpv_codes"
|
||||
shouldAddWithSpace
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
placeholder=""
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "tags.certifications",
|
||||
render: ({ register }) => (
|
||||
<TagInput<ICreateCompanyCredentials>
|
||||
{...register("tags.certifications")}
|
||||
label="Certifications"
|
||||
name="tags.certifications"
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
placeholder=""
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "custom",
|
||||
name: "tags.specializations",
|
||||
className: "sm:col-span-2",
|
||||
render: ({ register, error }) => (
|
||||
<>
|
||||
<TagInput<ICreateCompanyCredentials>
|
||||
{...register("tags.specializations")}
|
||||
label="Specializations"
|
||||
name="tags.specializations"
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
placeholder=""
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1.5 text-sm font-medium text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
rootClassName: "lg:col-span-2",
|
||||
className: "md:grid-cols-1",
|
||||
fields: [
|
||||
{
|
||||
kind: "custom",
|
||||
name: "document_file_ids",
|
||||
render: () => (
|
||||
<CompanyDocuments
|
||||
companyId={editMode ? id : undefined}
|
||||
value={watch("document_file_ids") ?? []}
|
||||
onChange={(ids) => setValue("document_file_ids", ids)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user