feat(cms): Add filtering functionality to CMS list table

- Create CmsListFilters component with search and key filter fields
- Add filter modal to CMS table with open/close functionality
- Implement useForm hook integration for filter form management
- Update useCmsTablePresenter to handle filter state and search logic
- Modify useGetCmsList query hook to accept and pass filter parameters
- Update cms-service.getCms method to support query parameters
- Add URL-based query string management for filter persistence
- Integrate filter button in ListHeader component
- Add isMutating state tracking for loading indicator during search
This commit is contained in:
AmirReza Jamali
2025-11-23 10:51:55 +03:30
parent e13527fe81
commit 410ca5a737
5 changed files with 150 additions and 9 deletions
@@ -0,0 +1,70 @@
import InputGroup from "@/components/FormElements/InputGroup";
import {
FieldValues,
UseFormHandleSubmit,
UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
interface CmsListFiltersProps {
filterRegister: UseFormRegister<FieldValues>;
setIsFilterModalOpen: (isOpen: boolean) => void;
isMutating: number;
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
search: (data: any) => void;
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
}
const CmsListFilters = ({
filterRegister,
setIsFilterModalOpen,
isMutating,
handleFilterSubmit,
search,
}: CmsListFiltersProps) => {
return (
<form
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
onSubmit={handleFilterSubmit(search)}
>
<InputGroup
{...filterRegister("q")}
name="q"
label="search"
placeholder="Search"
type="search"
/>
<InputGroup
{...filterRegister("key")}
name="key"
label="key"
placeholder="Key"
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>
);
};
export default CmsListFilters;
+27 -1
View File
@@ -10,6 +10,8 @@ import { unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip";
import { TableHeader, TableRow } from "../../ui/table";
import useCmsTablePresenter from "./useCmsTablePresenter";
import CmsListFilters from "./CmsListFilters";
import Modal from "@/components/ui/modal";
const CmsTable = () => {
const {
@@ -23,10 +25,18 @@ const CmsTable = () => {
onConfirmDelete,
router,
pathname,
isFilterModalOpen,
setIsFilterModalOpen,
filterRegister,
handleFilterSubmit,
search,
watch,
setFilterValue,
isMutating,
} = useCmsTablePresenter();
return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader />
<ListHeader onFilter={() => setIsFilterModalOpen(true)} />
<Table>
<TableHeader>
<TableRow className="border-none uppercase [&>th]:text-start">
@@ -103,6 +113,22 @@ const CmsTable = () => {
</EmptyListWrapper>
</TableBody>
</Table>
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
classNames="w-full xl:w-1/2"
showButtons={false}
>
<CmsListFilters
setValue={setFilterValue}
filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number}
search={search}
setIsFilterModalOpen={setIsFilterModalOpen}
watch={watch}
/>
</Modal>
{isModalOpen && (
<ConfirmationModal
isOpen={isModalOpen}
@@ -1,20 +1,55 @@
"use client";
import { useDeleteCms, useGetCmsList } from "@/hooks/queries/useCmsQueries";
import { TCms } from "@/lib/api/types/TCms";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
const useCmsTablePresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [currentCms, setCurrentCms] = useState<TCms | null>(null);
const [params, setParams] = useState<Record<string, any>>({});
const columns = ["row", "key", "created at", "actions"];
const { isPending, data } = useGetCmsList();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const isMutating = useIsMutating();
const {
register: filterRegister,
handleSubmit: handleFilterSubmit,
reset: filterFormReset,
watch,
setValue: setFilterValue,
} = useForm();
useEffect(() => {
const newParams: Record<string, any> = {};
for (const [key, value] of searchParams.entries()) {
newParams[key] = value;
}
setParams(newParams);
filterFormReset(newParams);
}, [searchParams, filterFormReset]);
const { isPending, data } = useGetCmsList(params);
const { mutate: deleteCms } = useDeleteCms(() => {
setIsModalOpen(false);
setCurrentCms(null);
});
const search = (data: any) => {
const newParams = { ...params, ...data };
const cleanedParams = deleteEmptyKeys(newParams);
const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathname}?${queryString}`);
setIsFilterModalOpen(false);
};
const onConfirmDelete = () => {
if (!currentCms?.id) return;
deleteCms(currentCms.id);
@@ -30,6 +65,14 @@ const useCmsTablePresenter = () => {
onConfirmDelete,
router,
pathname,
isFilterModalOpen,
setIsFilterModalOpen,
filterRegister,
handleFilterSubmit,
search,
watch,
setFilterValue,
isMutating,
};
};
+3 -3
View File
@@ -5,11 +5,11 @@ import { useRouter } from "next/navigation";
import { useMemo } from "react";
import { toast } from "react-toastify";
export const useGetCmsList = () => {
const queryKey = useMemo(() => [API_ENDPOINTS.CMS.BASE()], []);
export const useGetCmsList = (params?: Record<string, any>) => {
const queryKey = useMemo(() => [API_ENDPOINTS.CMS.BASE(), params], [params]);
return useQuery({
queryKey,
queryFn: cmsService.getCms,
queryFn: () => cmsService.getCms(params),
});
};
export const useGetCmsDetails = (id: string) => {
+4 -2
View File
@@ -4,9 +4,11 @@ import { ApiResponse } from "../types";
import { TCms, TCmsListResponse } from "../types/TCms";
export const cmsService = {
getCms: async (): Promise<ApiResponse<TCmsListResponse>> => {
getCms: async (
params?: Record<string, any>,
): Promise<ApiResponse<TCmsListResponse>> => {
try {
return (await api.get(API_ENDPOINTS.CMS.BASE())).data;
return (await api.get(API_ENDPOINTS.CMS.BASE(), { params })).data;
} catch (error) {
console.error("Error Caught in CMS Service => Get cms", error);
throw error;