diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx index 7b39d6a6d8..cf2950d7f2 100644 --- a/ui/components/filters/custom-date-picker.tsx +++ b/ui/components/filters/custom-date-picker.tsx @@ -8,12 +8,14 @@ import { } from "@internationalized/date"; import { Button, ButtonGroup, DatePicker } from "@nextui-org/react"; import { useLocale } from "@react-aria/i18n"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import React, { useCallback, useEffect, useRef } from "react"; +import { useUrlFilters } from "@/hooks/use-url-filters"; + export const CustomDatePicker = () => { - const router = useRouter(); const searchParams = useSearchParams(); + const { updateFilter } = useUrlFilters(); const [value, setValue] = React.useState(() => { const dateParam = searchParams.get("filter[inserted_at]"); @@ -28,15 +30,13 @@ export const CustomDatePicker = () => { const applyDateFilter = useCallback( (date: any) => { - const params = new URLSearchParams(searchParams.toString()); if (date) { - params.set("filter[inserted_at]", date.toString()); + updateFilter("inserted_at", date.toString()); } else { - params.delete("filter[inserted_at]"); + updateFilter("inserted_at", null); } - router.push(`?${params.toString()}`, { scroll: false }); }, - [router, searchParams], + [updateFilter], ); const initialRender = useRef(true); diff --git a/ui/components/filters/custom-search-input.tsx b/ui/components/filters/custom-search-input.tsx index 14d23bd812..0048236cc9 100644 --- a/ui/components/filters/custom-search-input.tsx +++ b/ui/components/filters/custom-search-input.tsx @@ -1,26 +1,25 @@ import { Input } from "@nextui-org/react"; import debounce from "lodash.debounce"; import { SearchIcon, XCircle } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import React, { useCallback, useEffect, useState } from "react"; -export const CustomSearchInput: React.FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); +import { useUrlFilters } from "@/hooks/use-url-filters"; +export const CustomSearchInput: React.FC = () => { + const searchParams = useSearchParams(); + const { updateFilter } = useUrlFilters(); const [searchQuery, setSearchQuery] = useState(""); const applySearch = useCallback( (query: string) => { - const params = new URLSearchParams(searchParams.toString()); if (query) { - params.set("filter[search]", query); + updateFilter("search", query); } else { - params.delete("filter[search]"); + updateFilter("search", null); } - router.push(`?${params.toString()}`, { scroll: false }); }, - [router, searchParams], + [updateFilter], ); const debouncedChangeHandler = useCallback( diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index 7a41bc37d5..35dddb0b37 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -1,8 +1,9 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import { useUrlFilters } from "@/hooks/use-url-filters"; import { FilterControlsProps } from "@/types"; import { CrossIcon } from "../icons"; @@ -24,8 +25,8 @@ export const FilterControls: React.FC = ({ mutedFindings = false, customFilters, }) => { - const router = useRouter(); const searchParams = useSearchParams(); + const { clearAllFilters } = useUrlFilters(); const [showClearButton, setShowClearButton] = useState(false); useEffect(() => { @@ -35,16 +36,6 @@ export const FilterControls: React.FC = ({ setShowClearButton(hasFilters); }, [searchParams]); - const clearAllFilters = useCallback(() => { - const params = new URLSearchParams(searchParams.toString()); - Array.from(params.keys()).forEach((key) => { - if (key.startsWith("filter[") || key === "sort") { - params.delete(key); - } - }); - router.push(`?${params.toString()}`, { scroll: false }); - }, [router, searchParams]); - return (
diff --git a/ui/components/ui/custom/custom-dropdown-filter.tsx b/ui/components/ui/custom/custom-dropdown-filter.tsx index 8fc213f75f..7f659d9c13 100644 --- a/ui/components/ui/custom/custom-dropdown-filter.tsx +++ b/ui/components/ui/custom/custom-dropdown-filter.tsx @@ -11,10 +11,11 @@ import { ScrollShadow, } from "@nextui-org/react"; import { XCircle } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { PlusCircleIcon } from "@/components/icons"; +import { useUrlFilters } from "@/hooks/use-url-filters"; import { CustomDropdownFilterProps } from "@/types"; const filterSelectedClass = @@ -24,8 +25,8 @@ export const CustomDropdownFilter: React.FC = ({ filter, onFilterChange, }) => { - const router = useRouter(); const searchParams = useSearchParams(); + const { clearFilter } = useUrlFilters(); const [groupSelected, setGroupSelected] = useState(new Set()); const [pendingClearFilter, setPendingClearFilter] = useState( null, @@ -114,13 +115,11 @@ export const CustomDropdownFilter: React.FC = ({ // Execute the update in the router after the render useEffect(() => { - if (pendingClearFilter) { - const params = new URLSearchParams(searchParams.toString()); - params.delete(`filter[${pendingClearFilter}]`); - router.push(`?${params.toString()}`, { scroll: false }); + if (pendingClearFilter && filter) { + clearFilter(pendingClearFilter); setPendingClearFilter(null); // Reset the state } - }, [pendingClearFilter, searchParams, router]); + }, [pendingClearFilter, clearFilter, filter]); return (
diff --git a/ui/components/ui/table/data-table-filter-custom.tsx b/ui/components/ui/table/data-table-filter-custom.tsx index 3ef0c0d330..655297659c 100644 --- a/ui/components/ui/table/data-table-filter-custom.tsx +++ b/ui/components/ui/table/data-table-filter-custom.tsx @@ -1,11 +1,11 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; import React, { useState } from "react"; import { useCallback } from "react"; import { CustomFilterIcon } from "@/components/icons"; import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom"; +import { useUrlFilters } from "@/hooks/use-url-filters"; import { FilterOption } from "@/types"; export interface DataTableFilterCustomProps { @@ -17,24 +17,14 @@ export const DataTableFilterCustom = ({ filters, defaultOpen = false, }: DataTableFilterCustomProps) => { - const router = useRouter(); - const searchParams = useSearchParams(); + const { updateFilter } = useUrlFilters(); const [showFilters, setShowFilters] = useState(defaultOpen); const pushDropdownFilter = useCallback( (key: string, values: string[]) => { - const params = new URLSearchParams(searchParams); - const filterKey = `filter[${key}]`; - - if (values.length === 0) { - params.delete(filterKey); - } else { - params.set(filterKey, values.join(",")); - } - - router.push(`?${params.toString()}`); + updateFilter(key, values.length > 0 ? values : null); }, - [router, searchParams], + [updateFilter], ); return ( diff --git a/ui/hooks/use-url-filters.ts b/ui/hooks/use-url-filters.ts new file mode 100644 index 0000000000..c29de37327 --- /dev/null +++ b/ui/hooks/use-url-filters.ts @@ -0,0 +1,68 @@ +"use client"; + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback } from "react"; + +/** + * Custom hook to handle URL filters and automatically reset + * pagination when filters change. + */ +export const useUrlFilters = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + + const updateFilter = useCallback( + (key: string, value: string | string[] | null) => { + const params = new URLSearchParams(searchParams.toString()); + + // Always reset page to 1 when a filter is applied + params.set("page", "1"); + + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + + if (value === null || (Array.isArray(value) && value.length === 0)) { + params.delete(filterKey); + } else if (Array.isArray(value)) { + params.set(filterKey, value.join(",")); + } else { + params.set(filterKey, value); + } + + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [router, searchParams, pathname], + ); + + const clearFilter = useCallback( + (key: string) => { + const params = new URLSearchParams(searchParams.toString()); + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + + params.delete(filterKey); + params.set("page", "1"); + + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [router, searchParams, pathname], + ); + + const clearAllFilters = useCallback(() => { + const params = new URLSearchParams(searchParams.toString()); + Array.from(params.keys()).forEach((key) => { + if (key.startsWith("filter[") || key === "sort") { + params.delete(key); + } + }); + + params.delete("page"); + + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, [router, searchParams, pathname]); + + return { + updateFilter, + clearFilter, + clearAllFilters, + }; +};