refactor(forms): Enhance MultiSelect with controlled state and styling

This commit overhauls the `MultiSelect` component to improve its functionality, flexibility, and integration with form libraries.

The component is now a controlled component, accepting a `value` prop and using the `onChange` handler to propagate updates. This allows for seamless integration with libraries like React Hook Form.

Key changes include:
- Added `value`, `disabled`, `active`, and `height` props for more granular control over the component's state and appearance.
- Refactored styling logic to use the `cn` utility for cleaner conditional class management.
- Simplified the internal JSX structure and SVGs for better readability and maintenance.
- Updated the component's state management to correctly reflect the incoming `value` prop.
This commit is contained in:
AmirReza Jamali
2025-09-20 18:28:53 +03:30
parent e6493f5d83
commit 0ea2ff635b
7 changed files with 388 additions and 159 deletions
-5
View File
@@ -1,15 +1,10 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { GlobeIcon } from "@/assets/icons";
import DatePickerOne from "@/components/FormElements/DatePicker/DatePickerOne"; import DatePickerOne from "@/components/FormElements/DatePicker/DatePickerOne";
import DatePickerTwo from "@/components/FormElements/DatePicker/DatePickerTwo"; import DatePickerTwo from "@/components/FormElements/DatePicker/DatePickerTwo";
import InputGroup from "@/components/FormElements/InputGroup";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { Checkbox } from "@/components/FormElements/checkbox"; import { Checkbox } from "@/components/FormElements/checkbox";
import { RadioInput } from "@/components/FormElements/radio"; import { RadioInput } from "@/components/FormElements/radio";
import { Select } from "@/components/FormElements/select";
import { Switch } from "@/components/FormElements/switch"; import { Switch } from "@/components/FormElements/switch";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
@@ -0,0 +1,188 @@
import { cn } from "@/lib/utils";
import { KeyboardEvent, useId, useState } from "react";
import {
type FieldErrors,
type FieldValues,
type Path,
type UseFormRegister,
type UseFormRegisterReturn,
type UseFormSetValue,
type UseFormWatch,
} from "react-hook-form";
import { toast } from "react-toastify";
type TagInputProps<T extends FieldValues> = {
name: Path<T>;
label: string;
placeholder: string;
className?: string;
required?: boolean;
disabled?: boolean;
active?: boolean;
value?: string[];
icon?: React.ReactNode;
iconPosition?: "left" | "right";
height?: "sm" | "default";
register?: UseFormRegister<T>;
setValue?: UseFormSetValue<T>;
watch?: UseFormWatch<T>;
errors?: FieldErrors<T>;
maxTags?: number;
allowDuplicates?: boolean;
tagValidator?: (tag: string) => boolean;
} & Partial<UseFormRegisterReturn>;
const TagInput = <T extends FieldValues>({
className,
label,
placeholder,
required,
disabled,
active,
icon,
name,
register,
setValue,
watch,
errors,
maxTags,
allowDuplicates = false,
tagValidator,
onChange,
onBlur,
ref,
...props
}: TagInputProps<T>): React.ReactElement => {
const id = useId();
const [inputValue, setInputValue] = useState("");
const watchedValue = watch ? watch(name) : props.value || [];
const tags: string[] = Array.isArray(watchedValue) ? watchedValue : [];
const error = errors && errors[name];
const addTag = (tag: string) => {
const trimmedTag = tag.trim();
if (!trimmedTag) return;
if (maxTags && tags.length >= maxTags) return;
if (!allowDuplicates && tags.includes(trimmedTag)) {
toast.error("Item already exists");
return;
}
if (tagValidator && !tagValidator(trimmedTag)) return;
const newTags = [...tags, trimmedTag];
if (setValue) {
setValue(name, newTags as any, { shouldValidate: true });
}
setInputValue("");
};
const removeTag = (indexToRemove: number) => {
const newTags = tags.filter((_, index) => index !== indexToRemove);
if (setValue) {
setValue(name, newTags as any, { shouldValidate: true });
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
addTag(inputValue);
} else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
removeTag(tags.length - 1);
}
};
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (inputValue) {
addTag(inputValue);
}
onBlur?.(e);
};
return (
<div className={className}>
<label
htmlFor={id}
className="text-body-sm font-medium text-dark dark:text-white"
>
{label}
{required && <span className="ml-1 select-none text-error">*</span>}
</label>
<div
className={cn(
"relative mt-3 [&_svg]:absolute [&_svg]:top-1/2 [&_svg]:-translate-y-1/2",
props.iconPosition === "left"
? "[&_svg]:left-4.5"
: "[&_svg]:right-4.5",
)}
>
<div
className={cn(
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus-within:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus-within:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
error && "border-red focus-within:border-red dark:border-red",
"flex min-h-[50px] flex-wrap items-center gap-2 px-3 py-2",
props.iconPosition === "left" && "pl-12.5",
props.height === "sm" && "min-h-[42px]",
disabled && "pointer-events-none opacity-60",
)}
data-active={active}
>
{tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-2.5 py-1 text-sm font-medium text-primary dark:bg-primary/20"
>
{tag}
{!disabled && (
<button
type="button"
onClick={() => removeTag(index)}
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
>
×
</button>
)}
</span>
))}
<input
id={id}
type="text"
placeholder={tags.length === 0 ? placeholder : ""}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
disabled={disabled || (maxTags ? tags.length >= maxTags : false)}
className="min-w-[120px] flex-1 bg-transparent text-dark outline-none placeholder:text-dark-6 dark:text-white"
/>
</div>
{icon}
<input
type="hidden"
{...(register ? register(name) : {})}
value={JSON.stringify(tags)}
ref={ref}
/>
</div>
{error && (
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
)}
{maxTags && (
<p className="mt-1 text-xs text-dark-6 dark:text-dark-6">
{tags.length}/{maxTags} tags
</p>
)}
</div>
);
};
export default TagInput;
+112 -91
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useEffect, useRef, useState, useId } from "react"; import { cn } from "@/lib/utils";
import React, { useEffect, useId, useRef, useState } from "react";
import { import {
type FieldErrors, type FieldErrors,
type FieldValues, type FieldValues,
@@ -17,10 +18,14 @@ interface Option {
type MultiSelectProps<T extends FieldValues> = { type MultiSelectProps<T extends FieldValues> = {
label: string; label: string;
items: { value: string; text: string }[]; items: { value: string; text: string }[];
value?: string[];
errors?: FieldErrors<T>; errors?: FieldErrors<T>;
required?: boolean; required?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean;
active?: boolean;
height?: "sm" | "default";
} & UseFormRegisterReturn<Path<T>>; } & UseFormRegisterReturn<Path<T>>;
function MultiSelect<T extends FieldValues>({ function MultiSelect<T extends FieldValues>({
@@ -28,12 +33,16 @@ function MultiSelect<T extends FieldValues>({
label, label,
items, items,
errors, errors,
required,
placeholder = "Select an option", placeholder = "Select an option",
className, className,
disabled,
active,
height,
onChange, onChange,
onBlur, onBlur,
ref, ref,
value,
required,
}: MultiSelectProps<T>) { }: MultiSelectProps<T>) {
const id = useId(); const id = useId();
const error = errors && errors[name]; const error = errors && errors[name];
@@ -45,16 +54,22 @@ function MultiSelect<T extends FieldValues>({
const trigger = useRef<HTMLDivElement | null>(null); const trigger = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
setOptions( const newOptions = items.map((item) => ({
items.map((item) => ({ ...item,
...item, selected: value?.includes(item.value) ?? false,
selected: false, }));
})), setOptions(newOptions);
);
}, [items]); const newSelectedIndices = items
.map((item, index) => (value?.includes(item.value) ? index : -1))
.filter((index) => index !== -1);
setSelected(newSelectedIndices);
}, [items, value]);
const open = () => { const open = () => {
setShow(true); if (!disabled) {
setShow(true);
}
}; };
const isOpen = () => { const isOpen = () => {
@@ -69,6 +84,8 @@ function MultiSelect<T extends FieldValues>({
}; };
const handleSelect = (index: number) => { const handleSelect = (index: number) => {
if (disabled) return;
const newOptions = [...options]; const newOptions = [...options];
let newSelected = [...selected]; let newSelected = [...selected];
@@ -83,11 +100,13 @@ function MultiSelect<T extends FieldValues>({
setOptions(newOptions); setOptions(newOptions);
setSelected(newSelected); setSelected(newSelected);
const selectedValues = newSelected.map((i) => options[i].value); const selectedValues = newSelected.map((i) => newOptions[i].value);
onChange?.({ target: { name, value: selectedValues } }); onChange?.({ target: { name, value: selectedValues } });
}; };
const remove = (index: number, event: React.MouseEvent) => { const remove = (index: number, event: React.MouseEvent) => {
if (disabled) return;
event.stopPropagation(); event.stopPropagation();
const newOptions = [...options]; const newOptions = [...options];
const newSelected = selected.filter((i) => i !== index); const newSelected = selected.filter((i) => i !== index);
@@ -96,7 +115,7 @@ function MultiSelect<T extends FieldValues>({
setOptions(newOptions); setOptions(newOptions);
setSelected(newSelected); setSelected(newSelected);
const selectedValues = newSelected.map((i) => options[i].value); const selectedValues = newSelected.map((i) => newOptions[i].value);
onChange?.({ target: { name, value: selectedValues } }); onChange?.({ target: { name, value: selectedValues } });
}; };
@@ -116,126 +135,128 @@ function MultiSelect<T extends FieldValues>({
}, [show]); }, [show]);
return ( return (
<div className={`relative z-50 ${className}`}> <div className={className}>
<label <label
htmlFor={id} htmlFor={id}
className="mb-3 block text-body-sm font-medium text-dark dark:text-white" className="text-body-sm font-medium text-dark dark:text-white"
> >
{label} {label}
{required && <span className="ml-1 select-none text-error">*</span>}
</label> </label>
<div className="relative"> <div className="relative mt-3">
<div <div
ref={combinedRef} ref={combinedRef}
onClick={open} onClick={open}
onBlur={onBlur} onBlur={onBlur}
tabIndex={0} tabIndex={0}
className={`flex min-h-[46px] w-full cursor-pointer items-center rounded-[7px] border-[1.5px] bg-transparent py-2 pl-3 pr-3 outline-none transition focus:border-primary active:border-primary dark:bg-dark-2 ${ className={cn(
error "w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
? "border-red focus:border-red dark:border-red" error && "border-red focus:border-red dark:border-red",
: "border-stroke dark:border-dark-3" "flex min-h-[50px] cursor-pointer flex-wrap items-center gap-2 px-3 py-2",
}`} height === "sm" && "min-h-[42px]",
disabled && "pointer-events-none opacity-60",
)}
data-active={active}
> >
<div className="flex flex-auto flex-wrap gap-2"> <div className="flex flex-1 flex-wrap items-center gap-2">
{selected.map((index) => ( {selected.map((index) => (
<div <span
key={index} key={index}
className="flex items-center justify-center rounded-[5px] border-[.5px] border-stroke bg-gray-2 px-2.5 py-1 text-body-sm font-medium dark:border-dark-3 dark:bg-dark" className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-2.5 py-1 text-sm font-medium text-primary dark:bg-primary/20"
> >
<div className="max-w-full flex-initial"> {options[index]?.text}
{options[index]?.text} {!disabled && (
</div> <button
<div className="flex flex-auto flex-row-reverse"> type="button"
<div
onClick={(e) => remove(index, e)} onClick={(e) => remove(index, e)}
className="hover:text-red cursor-pointer pl-2" className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
> >
<svg ×
className="fill-current" </button>
role="button" )}
width="12" </span>
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
))} ))}
{selected.length === 0 && ( {selected.length === 0 && (
<div className="flex-1"> <span className="text-dark-6 dark:text-dark-6">
<span className="h-full w-full bg-transparent p-1 px-2 text-dark-5 outline-none dark:text-dark-6"> {placeholder}
{placeholder} </span>
</span>
</div>
)} )}
</div> </div>
<div className="flex items-center py-1 pl-1 pr-1">
<button <div className="flex items-center">
type="button" <svg
className="cursor-pointer text-dark-4 outline-none focus:outline-none dark:text-dark-6" className={cn(
"fill-current text-dark-4 transition-transform duration-200 dark:text-dark-6",
isOpen() && "rotate-180",
)}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<svg <path
className={`fill-current transition-transform duration-200 ${ fillRule="evenodd"
isOpen() ? "rotate-180" : "" clipRule="evenodd"
}`} d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
width="20" fill="currentColor"
height="20" />
viewBox="0 0 20 20" </svg>
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
fill="currentColor"
/>
</svg>
</button>
</div> </div>
</div> </div>
<div <div
className={`max-h-select absolute left-0 top-full z-40 w-full overflow-y-auto rounded bg-white shadow-1 dark:bg-dark-2 dark:shadow-card ${ className={cn(
isOpen() ? "" : "hidden" "absolute left-0 top-full z-40 mt-1 max-h-60 w-full overflow-y-auto rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
}`} !isOpen() && "hidden",
)}
ref={dropdownRef} ref={dropdownRef}
> >
<div className="flex w-full flex-col"> <div className="py-1">
{options.map((option, index) => ( {options.map((option, index) => (
<div key={index}> <div
key={index}
className={cn(
"flex cursor-pointer items-center px-3 py-2 text-sm text-dark hover:bg-primary/5 dark:text-white dark:hover:bg-primary/5",
option.selected && "bg-primary/10 dark:bg-primary/20",
)}
onClick={() => handleSelect(index)}
>
<div <div
className="w-full cursor-pointer rounded-t border-b border-stroke hover:bg-primary/5 dark:border-dark-3" className={cn(
onClick={() => handleSelect(index)} "mr-3 h-4 w-4 rounded border-2",
option.selected
? "border-primary bg-primary"
: "border-stroke dark:border-dark-3",
)}
> >
<div {option.selected && (
className={`relative flex w-full items-center border-l-2 p-2 pl-2 ${ <svg
option.selected ? "border-primary" : "border-transparent" className="h-full w-full text-white"
}`} fill="currentColor"
> viewBox="0 0 20 20"
<div className="flex w-full items-center"> >
<div className="mx-2 leading-6">{option.text}</div> <path
</div> fillRule="evenodd"
</div> d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</div> </div>
{option.text}
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
{error && ( {error && (
<p className="text-red mt-1.5 text-sm">{error.message as string}</p> <p className="text-red mt-1.5 text-sm">{error.message as string}</p>
)} )}
</div> </div>
); );
} }
export default MultiSelect; export default MultiSelect;
@@ -1,7 +1,9 @@
"use client"; "use client";
import { GlobeIcon, MoneyIcon } from "@/assets/icons"; import { GlobeIcon, MoneyIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup"; import InputGroup from "@/components/FormElements/InputGroup";
import TagInput from "@/components/FormElements/InputGroup/tag-input";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area"; import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { Select } from "@/components/FormElements/select"; import { Select } from "@/components/FormElements/select";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import FormFooter from "@/components/ui/FormFooter"; import FormFooter from "@/components/ui/FormFooter";
@@ -17,12 +19,19 @@ interface IProps {
} }
const CreateCompany = ({ defaultValues, editMode, id }: IProps) => { const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
const { errors, handleSubmit, onSubmit, register } = const {
useCreateCompanyPresenter({ errors,
editMode, handleSubmit,
defaultValues, onSubmit,
id, register,
}); watch,
setValue,
companyCategories,
} = useCreateCompanyPresenter({
editMode,
defaultValues,
id,
});
return ( return (
<form <form
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2" className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2"
@@ -458,35 +467,56 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
)} )}
</ShowcaseSection> </ShowcaseSection>
<ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5"> <ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5">
<InputGroup <TagInput
{...register("tags.keywords")} {...register("tags.keywords")}
label="Keywords"
name="tags.keywords" name="tags.keywords"
type="text" label="Keywords"
placeholder="Enter Tag Keywords" placeholder="Enter Tag Keywords"
register={register}
setValue={setValue}
watch={watch}
/> />
{errors.tags?.keywords && ( <MultiSelect
<p className="mt-1 text-sm text-red-500">
{errors.tags.keywords.message}
</p>
)}
<InputGroup
{...register("tags.categories")} {...register("tags.categories")}
label="Categories" value={watch("tags.categories")}
name="tags.categories" name="tags.categories"
type="text" label="Categories"
placeholder="Enter Tag Categories" items={
companyCategories
? companyCategories?.data?.categories?.map((c) => {
return {
text: c.name,
value: c.id,
};
})
: []
}
placeholder="Please select categories"
/> />
{errors.tags?.categories && ( <TagInput
<p className="mt-1 text-sm text-red-500"> {...register("tags.cpv_codes")}
{errors.tags.categories.message} label="CPV codes"
</p> name="tags.cpv_codes"
)} watch={watch}
<InputGroup setValue={setValue}
placeholder="Enter Tag CPV codes"
/>
<TagInput
{...register("tags.certifications")}
label="Certifications"
name="tags.certifications"
watch={watch}
setValue={setValue}
placeholder="Enter Tag Certifications"
/>
<TagInput
{...register("tags.specializations")} {...register("tags.specializations")}
label="Specializations" label="Specializations"
name="tags.specializations" name="tags.specializations"
type="text" watch={watch}
setValue={setValue}
placeholder="Enter Tag Specializations" placeholder="Enter Tag Specializations"
/> />
{errors.tags?.specializations && ( {errors.tags?.specializations && (
@@ -1,5 +1,9 @@
"use client"; "use client";
import { useCreateCompany, useUpdateCompany } from "@/hooks/queries"; import {
useCompanyCategoriesQuery,
useCreateCompany,
useUpdateCompany,
} from "@/hooks/queries";
import { ICreateCompanyCredentials } from "@/lib/api"; import { ICreateCompanyCredentials } from "@/lib/api";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -17,9 +21,24 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
register, register,
formState: { errors }, formState: { errors },
handleSubmit, handleSubmit,
watch,
setValue,
reset, reset,
} = useForm<ICreateCompanyCredentials>({ mode: "onChange", defaultValues }); } = useForm<ICreateCompanyCredentials>({
mode: "onChange",
defaultValues: {
...defaultValues,
tags: {
...defaultValues?.tags,
categories: defaultValues?.tags.categories.map((item) => ({
value: item.id,
text: item.name,
})),
},
},
});
const router = useRouter(); const router = useRouter();
const { data: companyCategories } = useCompanyCategoriesQuery();
const { mutate: createCompany, isPending: isCreating } = useCreateCompany(); const { mutate: createCompany, isPending: isCreating } = useCreateCompany();
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany( const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
id as string, id as string,
@@ -30,33 +49,6 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
founded_year: data.founded_year ? +data.founded_year : 0, founded_year: data.founded_year ? +data.founded_year : 0,
employee_count: data.employee_count ? +data.employee_count : 0, employee_count: data.employee_count ? +data.employee_count : 0,
annual_revenue: data.annual_revenue ? +data.annual_revenue : 0, annual_revenue: data.annual_revenue ? +data.annual_revenue : 0,
tags: {
keywords: data.tags?.keywords
? (data.tags.keywords as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
categories: data.tags?.categories
? (data.tags.categories as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
specializations: data.tags?.specializations
? (data.tags.specializations as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
certifications: data.tags?.certifications
? (data.tags.certifications as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
cpv_codes: data.tags?.cpv_codes
? (data.tags.cpv_codes as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
},
}; };
if (editMode) { if (editMode) {
updateCompany({ updateCompany({
@@ -80,7 +72,10 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
isMutating, isMutating,
isCreating, isCreating,
isUpdating, isUpdating,
router router,
watch,
setValue,
companyCategories,
}; };
}; };
@@ -1,13 +1,13 @@
"use client"; "use client";
import { GlobeIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { Select } from "@/components/FormElements/select";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import { FormErrorMessages } from "@/constants/Texts";
import { CreateCustomerCredentials } from "@/lib/api"; import { CreateCustomerCredentials } from "@/lib/api";
import useCreateCustomerPresenter from "./useCreateCustomerPresenter"; import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import InputGroup from "@/components/FormElements/InputGroup";
import { FormErrorMessages } from "@/constants/Texts";
import { Select } from "@/components/FormElements/select";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { GlobeIcon, UserIcon } from "@/assets/icons";
interface IProps { interface IProps {
editMode?: boolean; editMode?: boolean;
+2 -2
View File
@@ -22,7 +22,7 @@ export interface IContactPerson {
export interface ITags { export interface ITags {
keywords: string[]; keywords: string[];
categories: string[]; categories: any[];
specializations: string[]; specializations: string[];
certifications: string[]; certifications: string[];
cpv_codes: string[]; cpv_codes: string[];
@@ -87,7 +87,7 @@ export const companySchema = z.object({
.optional(), .optional(),
tags: z.object({ tags: z.object({
keywords: z.array(z.string()).or(z.null()).optional(), keywords: z.array(z.string()).or(z.null()).optional(),
categories: z.array(z.string()).or(z.null()).optional(), categories: z.array(z.any()).or(z.null()).optional(),
specializations: z.array(z.string()).or(z.null()).optional(), specializations: z.array(z.string()).or(z.null()).optional(),
certifications: z.array(z.string()).or(z.null()).optional(), certifications: z.array(z.string()).or(z.null()).optional(),
cpv_codes: z.array(z.string()).or(z.null()).optional(), cpv_codes: z.array(z.string()).or(z.null()).optional(),