fix: keep search term on the url (DataTableToolbar.SearchBar) (#20521)

* fix: keep search term on the url (DataTableToolbar.SearchBar)

* update comment

* Update packages/features/data-table/components/DataTableToolbar.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Eunjae Lee
2025-04-03 12:59:36 +02:00
committed by GitHub
co-authored by sean-brydon
parent f353fdf252
commit 1e7453edcd
7 changed files with 67 additions and 80 deletions
@@ -1,9 +1,11 @@
"use client";
import type { SortingState, OnChangeFn, VisibilityState, ColumnSizingState } from "@tanstack/react-table";
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
import { usePathname } from "next/navigation";
import { useQueryState, parseAsArrayOf, parseAsJson, parseAsInteger } from "nuqs";
import { createContext, useCallback, useEffect, useRef } from "react";
import { useQueryState, parseAsArrayOf, parseAsJson, parseAsInteger, parseAsString } from "nuqs";
import { createContext, useCallback, useEffect, useRef, useMemo } from "react";
import { useSegments } from "./lib/segments";
import {
@@ -49,6 +51,9 @@ export type DataTableContextType = {
segmentId: number | undefined;
setSegmentId: (id: number | null) => void;
canSaveSegment: boolean;
searchTerm: string;
setSearchTerm: (searchTerm: string | null) => void;
};
export const DataTableContext = createContext<DataTableContextType | null>(null);
@@ -91,6 +96,12 @@ export function DataTableProvider({
const [segmentId, setSegmentId] = useQueryState("segment", parseAsInteger.withDefault(-1));
const [pageIndex, setPageIndex] = useQueryState("page", parseAsInteger.withDefault(0));
const [pageSize, setPageSize] = useQueryState("size", parseAsInteger.withDefault(defaultPageSize));
const [searchTerm, setSearchTerm] = useQueryState("q", parseAsString.withDefault(""));
const setDebouncedSearchTerm = useMemo(
() => debounce((value: string | null) => setSearchTerm(value ? value.trim() : null), 500),
[setSearchTerm]
);
const pathname = usePathname() as string | null;
const tableIdentifier = _tableIdentifier ?? pathname ?? undefined;
@@ -109,18 +120,6 @@ export function DataTableProvider({
[activeFilters, setActiveFilters]
);
const clearAll = useCallback(
(exclude?: string[]) => {
setSegmentIdAndSaveToLocalStorage(null);
setPageIndex(null);
setActiveFilters((prev) => {
const remainingFilters = prev.filter((filter) => exclude?.includes(filter.f));
return remainingFilters.length === 0 ? null : remainingFilters;
});
},
[setActiveFilters, setPageIndex]
);
const setPageIndexWrapper = useCallback(
(newPageIndex: number | null) => setPageIndex(newPageIndex || null),
[setPageIndex]
@@ -184,6 +183,18 @@ export function DataTableProvider({
setPageIndex,
});
const clearAll = useCallback(
(exclude?: string[]) => {
setSegmentIdAndSaveToLocalStorage(null);
setPageIndex(null);
setActiveFilters((prev) => {
const remainingFilters = prev.filter((filter) => exclude?.includes(filter.f));
return remainingFilters.length === 0 ? null : remainingFilters;
});
},
[setActiveFilters, setPageIndex, setSegmentIdAndSaveToLocalStorage]
);
const ctaContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (ctaContainerClassName) {
@@ -219,6 +230,8 @@ export function DataTableProvider({
segmentId: segmentId || undefined,
setSegmentId: setSegmentIdAndSaveToLocalStorage,
canSaveSegment,
searchTerm,
setSearchTerm: setDebouncedSearchTerm,
}}>
{children}
</DataTableContext.Provider>
@@ -1,16 +1,16 @@
"use client";
import type { Table } from "@tanstack/react-table";
import { useEffect, useState, forwardRef } from "react";
import { forwardRef } from "react";
import type { ComponentPropsWithoutRef } from "react";
import { useState, type Ref, type ChangeEvent } from "react";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Button, type ButtonProps } from "@calcom/ui/components/button";
import { Input } from "@calcom/ui/components/form";
import { useColumnFilters } from "../hooks";
import { useColumnFilters, useDataTable } from "../hooks";
interface DataTableToolbarProps extends ComponentPropsWithoutRef<"div"> {
children: React.ReactNode;
@@ -30,56 +30,34 @@ const Root = forwardRef<HTMLDivElement, DataTableToolbarProps>(function DataTabl
);
});
interface SearchBarProps<TData> {
table: Table<TData>;
searchKey?: string;
onSearch?: (value: string) => void;
interface SearchBarProps {
className?: string;
}
function SearchBarComponent<TData>(
{ table, searchKey, onSearch, className }: SearchBarProps<TData>,
ref: React.Ref<HTMLInputElement>
) {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
function SearchBarComponent({ className }: SearchBarProps, ref: Ref<HTMLInputElement>) {
const { searchTerm, setSearchTerm } = useDataTable();
const { t } = useLocale();
const [localValue, setLocalValue] = useState(searchTerm);
useEffect(() => {
onSearch?.(debouncedSearchTerm);
}, [debouncedSearchTerm, onSearch]);
if (onSearch) {
return (
<Input
ref={ref}
className={`max-w-48 ${className ?? ""}`}
placeholder="Search"
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value.trim())}
/>
);
}
if (!searchKey) {
console.error("searchKey is required if onSearch is not provided");
return null;
}
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setLocalValue(value);
setSearchTerm(value);
};
return (
<Input
ref={ref}
className="max-w-48"
placeholder="Search"
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
onChange={(event) => {
return table.getColumn(searchKey)?.setFilterValue(event.target.value.trim());
}}
className={classNames("max-w-48" ,className)}
placeholder={t("search")}
value={localValue}
onChange={handleSearchChange}
/>
);
}
const SearchBar = forwardRef(SearchBarComponent) as <TData>(
props: SearchBarProps<TData> & { ref?: React.Ref<HTMLInputElement> }
const SearchBar = forwardRef(SearchBarComponent) as (
props: SearchBarProps & { ref?: React.Ref<HTMLInputElement> }
) => ReturnType<typeof SearchBarComponent>;
interface ClearFiltersButtonProps<TData> {
@@ -24,6 +24,7 @@ import {
DataTableToolbar,
DataTableFilters,
DataTableSelectionBar,
useDataTable,
useFetchMoreOnBottomReached,
useColumnFilters,
} from "@calcom/features/data-table";
@@ -172,13 +173,14 @@ function MemberListContent(props: Props) {
const tableContainerRef = useRef<HTMLDivElement>(null);
const [state, dispatch] = useReducer(reducer, initialState);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const { searchTerm } = useDataTable();
const { data, isPending, hasNextPage, fetchNextPage, isFetching } =
trpc.viewer.teams.listMembers.useInfiniteQuery(
{
limit: 10,
searchTerm: debouncedSearchTerm,
searchTerm,
teamId: props.team.id,
// TODO: send `columnFilters` to server for server side filtering
// filters: columnFilters,
@@ -238,7 +240,7 @@ function MemberListContent(props: Props) {
const previousValue = utils.viewer.teams.listMembers.getInfiniteData({
limit: 10,
teamId: teamIds[0],
searchTerm: debouncedSearchTerm,
searchTerm,
});
if (previousValue) {
@@ -246,7 +248,7 @@ function MemberListContent(props: Props) {
utils,
memberId: state.deleteMember.user?.id as number,
teamId: teamIds[0],
searchTerm: debouncedSearchTerm,
searchTerm,
});
}
return { previousValue };
@@ -667,7 +669,7 @@ function MemberListContent(props: Props) {
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
<DataTableToolbar.Root>
<div className="flex w-full gap-2">
<DataTableToolbar.SearchBar table={table} onSearch={(value) => setDebouncedSearchTerm(value)} />
<DataTableToolbar.SearchBar />
<DataTableFilters.AddFilterButton table={table} />
<DataTableFilters.ColumnVisibilityButton table={table} />
{isAdminOrOwner && (
@@ -16,6 +16,7 @@ import {
DataTableToolbar,
DataTableProvider,
ColumnFilterType,
useDataTable,
useFilterValue,
ZDateRangeFilterValue,
DataTableFilters,
@@ -71,7 +72,6 @@ export default function OutOfOfficeEntriesList() {
function OutOfOfficeEntriesListContent() {
const { t } = useLocale();
const tableContainerRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState("");
const [deletedEntry, setDeletedEntry] = useState(0);
const [currentlyEditingOutOfOfficeEntry, setCurrentlyEditingOutOfOfficeEntry] =
useState<BookingRedirectForm | null>(null);
@@ -81,6 +81,7 @@ function OutOfOfficeEntriesListContent() {
setOpenModal(true);
};
const { searchTerm } = useDataTable();
const searchParams = useCompatSearchParams();
const selectedTab = searchParams?.get("type") ?? OutOfOfficeTab.MINE;
@@ -348,7 +349,7 @@ function OutOfOfficeEntriesListContent() {
paginationMode="infinite"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar table={table} onSearch={(value) => setSearchTerm(value)} />
<DataTableToolbar.SearchBar />
<DataTableFilters.FilterBar table={table} />
</>
}
@@ -8,9 +8,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import dayjs from "@calcom/dayjs";
import { DataTableProvider } from "@calcom/features/data-table/DataTableProvider";
import { DataTable, DataTableToolbar } from "@calcom/features/data-table/components";
import { useDataTable } from "@calcom/features/data-table/hooks";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
import type { MembershipRole } from "@calcom/prisma/enums";
@@ -80,8 +80,7 @@ function AvailabilitySliderTableContent(props: { userTimeFormat: number | null;
const [browsingDate, setBrowsingDate] = useState(dayjs());
const [editSheetOpen, setEditSheetOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<SliderUser | null>(null);
const [searchString, setSearchString] = useState("");
const debouncedSearchString = useDebounce(searchString, 500);
const { searchTerm } = useDataTable();
const tbStore = createTimezoneBuddyStore({
browsingDate: browsingDate.toDate(),
@@ -93,7 +92,7 @@ function AvailabilitySliderTableContent(props: { userTimeFormat: number | null;
loggedInUsersTz: CURRENT_TIMEZONE,
startDate: browsingDate.startOf("day").toISOString(),
endDate: browsingDate.endOf("day").toISOString(),
searchString: debouncedSearchString,
searchString: searchTerm,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
@@ -253,7 +252,7 @@ function AvailabilitySliderTableContent(props: { userTimeFormat: number | null;
isPending={isPending}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
<DataTableToolbar.Root>
<DataTableToolbar.SearchBar table={table} onSearch={(value) => setSearchString(value)} />
<DataTableToolbar.SearchBar />
</DataTableToolbar.Root>
</DataTable>
</CellHighlightContainer>
@@ -68,12 +68,11 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps)
const { t } = useLocale();
const [state, dispatch] = useReducer(reducer, initialState);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [rowSelection, setRowSelection] = useState({});
const columnFilters = useColumnFilters();
const { pageIndex, pageSize } = useDataTable();
const { pageIndex, pageSize, searchTerm } = useDataTable();
const limit = pageSize;
const offset = pageIndex * pageSize;
@@ -81,7 +80,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps)
{
limit,
offset,
searchTerm: debouncedSearchTerm,
searchTerm,
filters: columnFilters,
oAuthClientId,
},
@@ -289,11 +288,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps)
paginationMode="standard"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar
table={table}
onSearch={(value) => setDebouncedSearchTerm(value)}
className="sm:max-w-64 max-w-full"
/>
<DataTableToolbar.SearchBar className="sm:max-w-64 max-w-full" />
<DataTableFilters.ColumnVisibilityButton table={table} />
<DataTableFilters.FilterBar table={table} />
</>
@@ -134,19 +134,18 @@ function UserListTableContent() {
});
const [state, dispatch] = useReducer(reducer, initialState);
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [isDownloading, setIsDownloading] = useState(false);
const [rowSelection, setRowSelection] = useState({});
const columnFilters = useColumnFilters();
const { limit, offset, ctaContainerRef } = useDataTable();
const { limit, offset, searchTerm, ctaContainerRef } = useDataTable();
const { data, isPending } = trpc.viewer.organizations.listMembers.useQuery(
{
limit,
offset,
searchTerm: debouncedSearchTerm,
searchTerm,
expand: ["attributes"],
filters: columnFilters,
},
@@ -483,7 +482,7 @@ function UserListTableContent() {
const result = await utils.viewer.organizations.listMembers.fetch({
limit,
offset,
searchTerm: debouncedSearchTerm,
searchTerm,
expand: ["attributes"],
filters: columnFilters,
});
@@ -531,7 +530,7 @@ function UserListTableContent() {
paginationMode="standard"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar table={table} onSearch={(value) => setDebouncedSearchTerm(value)} />
<DataTableToolbar.SearchBar />
<DataTableFilters.ColumnVisibilityButton table={table} />
<DataTableFilters.FilterBar table={table} />
</>