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:
AmirReza Jamali
2025-10-13 12:26:56 +03:30
parent 5b73389d64
commit f84c21f829
8 changed files with 257 additions and 116 deletions
@@ -1,57 +1,95 @@
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import { AdminRoles, NotificationChannels } from "@/constants/enums";
import { AdminRoles, AdminStatus } from "@/constants/enums";
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 { register } = useAdminListFilterPresenter();
const AdminListFilters = ({
filterRegister,
setIsFilterModalOpen,
isMutating,
handleFilterSubmit,
search,
}: AdminListFiltersProps) => {
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
{...register("method")}
items={getEnumAsArray(NotificationChannels)}
label="Method"
name="method"
placeholder="Method"
/>
<Select
{...register("status")}
items={getEnumAsArray(NotificationChannels)}
{...filterRegister("status")}
items={getEnumAsArray(AdminStatus)}
label="status"
name="status"
placeholder="Status"
clearable
/>
<InputGroup
{...register("email")}
{...filterRegister("search")}
name="search"
label="search"
placeholder="search"
type="search"
/>
{/* <InputGroup
{...filterRegister("email")}
name="email"
label="email"
placeholder="email"
type="email"
/>
/> */}
<Select
{...register("role")}
{...filterRegister("role")}
name="role"
items={getEnumAsArray(AdminRoles)}
label="Role"
placeholder="Role"
/>
<InputGroup
{...register("username")}
{/* <InputGroup
{...filterRegister("username")}
label="username"
name="username"
placeholder="username"
type="text"
/>
<InputGroup
{...register("full_name")}
/> */}
{/* <InputGroup
{...filterRegister("full_name")}
label="full name"
name="full_name"
placeholder="full name"
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>
);
};
+93 -74
View File
@@ -39,6 +39,10 @@ const AdminsTable = () => {
currentUser,
setStatus,
status,
handleFilterSubmit,
search,
filterRegister,
isMutating,
} = useAdminsPresenter();
return (
@@ -59,79 +63,87 @@ const AdminsTable = () => {
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
{allUsers.map((item, index) => {
return (
<TableRow
key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.full_name}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.email}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.role}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.username}
</TableCell>
<TableCell className="text-start" colSpan={100}>
<Status status={item.status ?? ""}>{item.status}</Status>
</TableCell>
{!allUsers.length ? (
<TableRow>
<TableCell>
<h2>No user</h2>
</TableCell>
</TableRow>
) : (
allUsers.map((item, index) => {
return (
<TableRow
key={item?.id || index}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.full_name}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.email}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.role}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.username}
</TableCell>
<TableCell className="text-start" colSpan={100}>
<Status status={item?.status ?? ""}>{item?.status}</Status>
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentUser(item);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="change-status"
data-tooltip-content="Change status"
data-tooltip-place="top"
className="hover:text-yellow-dark"
onClick={() => {
setCurrentUser(item);
setIsChangeStatusModalOpen(true);
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "change-status" })}
/>
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() => router.push(`/admins/edit/${item.id}`)}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Admin </span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
);
})}
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentUser(item);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="change-status"
data-tooltip-content="Change status"
data-tooltip-place="top"
className="hover:text-yellow-dark"
onClick={() => {
setCurrentUser(item);
setIsChangeStatusModalOpen(true);
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "change-status" })}
/>
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() => router.push(`/admins/edit/${item.id}`)}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Admin </span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<Modal
@@ -150,9 +162,16 @@ const AdminsTable = () => {
<Modal
isOpen={isFilterModalOpen}
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>
{isModalOpen && (
<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 { AdminStatus } from "@/constants/enums";
import { deleteEmptyKeys } from "@/utils/shared";
import {
useAdminListInfiniteQuery,
useChangeAdminStatus,
useDeleteAdmin,
useGetAdminListQuery,
} 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 { useState } from "react";
import { useForm } from "react-hook-form";
@@ -20,7 +22,12 @@ export const useAdminsPresenter = () => {
const [status, setStatus] = useState<string>(
currentUser?.status ?? AdminStatus.ACTIVE,
);
const isMutating = useIsMutating();
const router = useRouter();
const { register: filterRegister, handleSubmit: handleFilterSubmit } =
useForm();
const queryClient = useQueryClient();
const {
register,
formState: { errors },
@@ -33,9 +40,11 @@ export const useAdminsPresenter = () => {
});
const [params, setParams] = useState<Record<string, any>>({
...apiDefaultParams,
offset: 0,
limit: 10,
});
const pathName = usePathname();
const { data, isPending } = useAdminListInfiniteQuery(params);
const { data, isPending } = useGetAdminListQuery(params);
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
setIsModalOpen(false);
});
@@ -51,7 +60,16 @@ export const useAdminsPresenter = () => {
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 = [
"row",
"full name",
@@ -63,11 +81,13 @@ export const useAdminsPresenter = () => {
];
return {
allUsers,
metadata,
columns,
isPending,
onConfirmDelete,
isModalOpen,
setIsModalOpen,
isMutating,
router,
setCurrentUser,
pathName,
@@ -82,5 +102,9 @@ export const useAdminsPresenter = () => {
handleSubmit,
isFilterModalOpen,
setIsFilterModalOpen,
setParams,
filterRegister,
handleFilterSubmit,
search,
};
};