feat(form): Add clearable functionality to Select component
This commit introduces a new `clearable` prop to the `Select` component, allowing users to easily reset its value. When the `clearable` prop is set to true, a clear button (an 'X' icon) is displayed when an option is selected. Clicking this button resets the select input to its initial placeholder state and triggers an optional `onClear` callback. To support this, the component's internal state management was refactored to control the input's value. Additionally, the `AdminListFilters` component has been updated to: - Utilize the new clearable select for the status filter. - Consolidate multiple search fields (email, username, full_name) into a single generic "search" input. - Add explicit "Reset" and "Apply" buttons for better filter management.
This commit is contained in:
@@ -22,6 +22,8 @@ type PropsType<T extends FieldValues> = {
|
|||||||
register?: UseFormRegister<T>;
|
register?: UseFormRegister<T>;
|
||||||
errors?: FieldErrors<T>;
|
errors?: FieldErrors<T>;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
|
onClear?: () => void;
|
||||||
} & (
|
} & (
|
||||||
| { placeholder?: string; defaultValue: string }
|
| { placeholder?: string; defaultValue: string }
|
||||||
| { placeholder: string; defaultValue?: string }
|
| { placeholder: string; defaultValue?: string }
|
||||||
@@ -42,13 +44,22 @@ export function Select<T extends FieldValues>({
|
|||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
ref,
|
ref,
|
||||||
|
clearable = false,
|
||||||
|
onClear,
|
||||||
...props
|
...props
|
||||||
}: PropsType<T>) {
|
}: PropsType<T>) {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
|
const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
|
||||||
|
const [value, setValue] = useState(defaultValue || "");
|
||||||
const error = errors && errors[name];
|
const error = errors && errors[name];
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setValue("");
|
||||||
|
setIsOptionSelected(false);
|
||||||
|
onClear?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-3", className)}>
|
<div className={cn("space-y-3", className)}>
|
||||||
<label
|
<label
|
||||||
@@ -70,8 +81,9 @@ export function Select<T extends FieldValues>({
|
|||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
defaultValue={defaultValue || ""}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
setIsOptionSelected(!!e.target.value);
|
setIsOptionSelected(!!e.target.value);
|
||||||
onChange?.(e);
|
onChange?.(e);
|
||||||
}}
|
}}
|
||||||
@@ -81,6 +93,7 @@ export function Select<T extends FieldValues>({
|
|||||||
isOptionSelected && "text-dark dark:text-white",
|
isOptionSelected && "text-dark dark:text-white",
|
||||||
error && "border-red focus:border-red dark:border-red",
|
error && "border-red focus:border-red dark:border-red",
|
||||||
prefixIcon && "pl-11.5",
|
prefixIcon && "pl-11.5",
|
||||||
|
clearable && value && "pr-20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{placeholder && (
|
{placeholder && (
|
||||||
@@ -96,6 +109,30 @@ export function Select<T extends FieldValues>({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{clearable && value && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="absolute right-10 top-1/2 -translate-y-1/2 text-dark-5 hover:text-dark dark:text-dark-6 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 4L4 12M4 4L12 12"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180" />
|
<ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,95 @@
|
|||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
import { Select } from "@/components/FormElements/select";
|
import { Select } from "@/components/FormElements/select";
|
||||||
import { AdminRoles, NotificationChannels } from "@/constants/enums";
|
import { AdminRoles, AdminStatus } from "@/constants/enums";
|
||||||
import { getEnumAsArray } from "@/utils/shared";
|
import { getEnumAsArray } from "@/utils/shared";
|
||||||
import useAdminListFilterPresenter from "./useAdminListFilterPresenter";
|
import {
|
||||||
|
FieldValues,
|
||||||
|
UseFormHandleSubmit,
|
||||||
|
UseFormRegister,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
interface IProps {}
|
interface AdminListFiltersProps {
|
||||||
|
filterRegister: UseFormRegister<FieldValues>;
|
||||||
|
setIsFilterModalOpen: (isOpen: boolean) => void;
|
||||||
|
isMutating: number;
|
||||||
|
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
|
||||||
|
search: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const AdminListFilters = ({}: IProps) => {
|
const AdminListFilters = ({
|
||||||
const { register } = useAdminListFilterPresenter();
|
filterRegister,
|
||||||
|
setIsFilterModalOpen,
|
||||||
|
isMutating,
|
||||||
|
handleFilterSubmit,
|
||||||
|
search,
|
||||||
|
}: AdminListFiltersProps) => {
|
||||||
return (
|
return (
|
||||||
<form className="grid !min-w-full grid-cols-3 gap-4">
|
<form
|
||||||
|
className="grid !min-w-full grid-cols-3 gap-4"
|
||||||
|
onSubmit={handleFilterSubmit(search)}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
{...register("method")}
|
{...filterRegister("status")}
|
||||||
items={getEnumAsArray(NotificationChannels)}
|
items={getEnumAsArray(AdminStatus)}
|
||||||
label="Method"
|
|
||||||
name="method"
|
|
||||||
placeholder="Method"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
{...register("status")}
|
|
||||||
items={getEnumAsArray(NotificationChannels)}
|
|
||||||
label="status"
|
label="status"
|
||||||
name="status"
|
name="status"
|
||||||
placeholder="Status"
|
placeholder="Status"
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
{...register("email")}
|
{...filterRegister("search")}
|
||||||
|
name="search"
|
||||||
|
label="search"
|
||||||
|
placeholder="search"
|
||||||
|
type="search"
|
||||||
|
/>
|
||||||
|
{/* <InputGroup
|
||||||
|
{...filterRegister("email")}
|
||||||
name="email"
|
name="email"
|
||||||
label="email"
|
label="email"
|
||||||
placeholder="email"
|
placeholder="email"
|
||||||
type="email"
|
type="email"
|
||||||
/>
|
/> */}
|
||||||
<Select
|
<Select
|
||||||
{...register("role")}
|
{...filterRegister("role")}
|
||||||
name="role"
|
name="role"
|
||||||
items={getEnumAsArray(AdminRoles)}
|
items={getEnumAsArray(AdminRoles)}
|
||||||
label="Role"
|
label="Role"
|
||||||
placeholder="Role"
|
placeholder="Role"
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
{/* <InputGroup
|
||||||
{...register("username")}
|
{...filterRegister("username")}
|
||||||
label="username"
|
label="username"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/> */}
|
||||||
<InputGroup
|
{/* <InputGroup
|
||||||
{...register("full_name")}
|
{...filterRegister("full_name")}
|
||||||
label="full name"
|
label="full name"
|
||||||
name="full_name"
|
name="full_name"
|
||||||
placeholder="full name"
|
placeholder="full name"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/> */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||||
|
>
|
||||||
|
{isMutating ? (
|
||||||
|
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||||
|
) : (
|
||||||
|
<span>Search</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||||
|
onClick={() => setIsFilterModalOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const AdminsTable = () => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
setStatus,
|
setStatus,
|
||||||
status,
|
status,
|
||||||
|
handleFilterSubmit,
|
||||||
|
search,
|
||||||
|
filterRegister,
|
||||||
|
isMutating,
|
||||||
} = useAdminsPresenter();
|
} = useAdminsPresenter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,29 +63,36 @@ const AdminsTable = () => {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
{isPending && <TableSkeleton column={columns.length} />}
|
{isPending && <TableSkeleton column={columns.length} />}
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{allUsers.map((item, index) => {
|
{!allUsers.length ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<h2>No user</h2>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
allUsers.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={item.id}
|
key={item?.id || index}
|
||||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||||
>
|
>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.full_name}
|
{item?.full_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.email}
|
{item?.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.role}
|
{item?.role}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{item.username}
|
{item?.username}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
<Status status={item.status ?? ""}>{item.status}</Status>
|
<Status status={item?.status ?? ""}>{item?.status}</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-start xl:pr-7.5">
|
<TableCell className="text-start xl:pr-7.5">
|
||||||
@@ -131,7 +142,8 @@ const AdminsTable = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -150,9 +162,16 @@ const AdminsTable = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isFilterModalOpen}
|
isOpen={isFilterModalOpen}
|
||||||
onClose={() => setIsFilterModalOpen(false)}
|
onClose={() => setIsFilterModalOpen(false)}
|
||||||
classNames="w-full"
|
classNames="w-3/4"
|
||||||
|
showButtons={false}
|
||||||
>
|
>
|
||||||
<AdminListFilters />
|
<AdminListFilters
|
||||||
|
filterRegister={filterRegister}
|
||||||
|
handleFilterSubmit={handleFilterSubmit}
|
||||||
|
isMutating={isMutating as number}
|
||||||
|
search={search}
|
||||||
|
setIsFilterModalOpen={setIsFilterModalOpen}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
const useAdminListFilterPresenter = () => {
|
|
||||||
const { register } = useForm();
|
|
||||||
return {
|
|
||||||
register,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useAdminListFilterPresenter;
|
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import { apiDefaultParams } from "@/constants/Api_Params";
|
import { apiDefaultParams } from "@/constants/Api_Params";
|
||||||
import { AdminStatus } from "@/constants/enums";
|
import { AdminStatus } from "@/constants/enums";
|
||||||
|
import { deleteEmptyKeys } from "@/utils/shared";
|
||||||
import {
|
import {
|
||||||
useAdminListInfiniteQuery,
|
|
||||||
useChangeAdminStatus,
|
useChangeAdminStatus,
|
||||||
useDeleteAdmin,
|
useDeleteAdmin,
|
||||||
|
useGetAdminListQuery,
|
||||||
} from "@/hooks/queries";
|
} from "@/hooks/queries";
|
||||||
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
|
import { API_ENDPOINTS, IUser, TChangeAdminStatusCredentials } from "@/lib/api";
|
||||||
|
import { useIsMutating, useQueryClient } from "@tanstack/react-query";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -20,7 +22,12 @@ export const useAdminsPresenter = () => {
|
|||||||
const [status, setStatus] = useState<string>(
|
const [status, setStatus] = useState<string>(
|
||||||
currentUser?.status ?? AdminStatus.ACTIVE,
|
currentUser?.status ?? AdminStatus.ACTIVE,
|
||||||
);
|
);
|
||||||
|
const isMutating = useIsMutating();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { register: filterRegister, handleSubmit: handleFilterSubmit } =
|
||||||
|
useForm();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@@ -33,9 +40,11 @@ export const useAdminsPresenter = () => {
|
|||||||
});
|
});
|
||||||
const [params, setParams] = useState<Record<string, any>>({
|
const [params, setParams] = useState<Record<string, any>>({
|
||||||
...apiDefaultParams,
|
...apiDefaultParams,
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
});
|
});
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
const { data, isPending } = useAdminListInfiniteQuery(params);
|
const { data, isPending } = useGetAdminListQuery(params);
|
||||||
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
|
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
});
|
});
|
||||||
@@ -51,7 +60,16 @@ export const useAdminsPresenter = () => {
|
|||||||
setIsChangeStatusModalOpen(false);
|
setIsChangeStatusModalOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const allUsers = data?.pages.flatMap((page) => page.data.users) ?? [];
|
|
||||||
|
const search = (data: any) => {
|
||||||
|
setParams((prevParams) => {
|
||||||
|
const newParams = { ...prevParams, ...data, offset: 0 };
|
||||||
|
return deleteEmptyKeys(newParams);
|
||||||
|
});
|
||||||
|
setIsFilterModalOpen(false);
|
||||||
|
};
|
||||||
|
const allUsers = data?.data?.users ?? [];
|
||||||
|
const metadata = data?.meta;
|
||||||
const columns = [
|
const columns = [
|
||||||
"row",
|
"row",
|
||||||
"full name",
|
"full name",
|
||||||
@@ -63,11 +81,13 @@ export const useAdminsPresenter = () => {
|
|||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
allUsers,
|
allUsers,
|
||||||
|
metadata,
|
||||||
columns,
|
columns,
|
||||||
isPending,
|
isPending,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
setIsModalOpen,
|
setIsModalOpen,
|
||||||
|
isMutating,
|
||||||
router,
|
router,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
pathName,
|
pathName,
|
||||||
@@ -82,5 +102,9 @@ export const useAdminsPresenter = () => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
isFilterModalOpen,
|
isFilterModalOpen,
|
||||||
setIsFilterModalOpen,
|
setIsFilterModalOpen,
|
||||||
|
setParams,
|
||||||
|
filterRegister,
|
||||||
|
handleFilterSubmit,
|
||||||
|
search,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
export * from "./useUsersQueries";
|
|
||||||
export * from "./useCompaniesQueries";
|
export * from "./useCompaniesQueries";
|
||||||
export * from "./useTenders";
|
|
||||||
export * from "./useCustomerQueries";
|
export * from "./useCustomerQueries";
|
||||||
|
export * from "./useFeedbackQueries";
|
||||||
export * from "./useProfileQueries";
|
export * from "./useProfileQueries";
|
||||||
export * from "./useFeedbackQueries"
|
export * from "./useTenders";
|
||||||
|
export * from "./useUsersQueries";
|
||||||
|
|
||||||
|
export * from "./useCompaniesQueries";
|
||||||
|
export * from "./useCustomerQueries";
|
||||||
|
export * from "./useFeedbackQueries";
|
||||||
|
export * from "./useFlagsQueries";
|
||||||
|
export * from "./useNotificationQueries";
|
||||||
|
export * from "./useProfileQueries";
|
||||||
|
export * from "./useTenders";
|
||||||
|
export * from "./useUsersQueries";
|
||||||
|
export * from "./useAdminListQuery";
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { API_ENDPOINTS, userService } from "@/lib/api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useGetAdminListQuery = (params?: Record<string, any>) => {
|
||||||
|
const queryKey = useMemo(() => [API_ENDPOINTS.USER.ADMINS, params], [params]);
|
||||||
|
return useQuery({
|
||||||
|
queryKey,
|
||||||
|
queryFn: () => userService.adminsList(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -98,3 +98,15 @@ export function parseWithSchema<T>(
|
|||||||
throw new Error(`ERROR caught in ${errorContext}`);
|
throw new Error(`ERROR caught in ${errorContext}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteEmptyKeys = (obj: Record<string, any>) => {
|
||||||
|
const newObj = { ...obj };
|
||||||
|
for (const key in newObj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||||
|
if (!newObj[key]) {
|
||||||
|
delete newObj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user