diff --git a/packages/features/data-table/DataTableProvider.tsx b/packages/features/data-table/DataTableProvider.tsx index 1516ab40a3..23384dea60 100644 --- a/packages/features/data-table/DataTableProvider.tsx +++ b/packages/features/data-table/DataTableProvider.tsx @@ -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(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(null); useEffect(() => { if (ctaContainerClassName) { @@ -219,6 +230,8 @@ export function DataTableProvider({ segmentId: segmentId || undefined, setSegmentId: setSegmentIdAndSaveToLocalStorage, canSaveSegment, + searchTerm, + setSearchTerm: setDebouncedSearchTerm, }}> {children} diff --git a/packages/features/data-table/components/DataTableToolbar.tsx b/packages/features/data-table/components/DataTableToolbar.tsx index 92f15f117f..2e324f466f 100644 --- a/packages/features/data-table/components/DataTableToolbar.tsx +++ b/packages/features/data-table/components/DataTableToolbar.tsx @@ -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(function DataTabl ); }); -interface SearchBarProps { - table: Table; - searchKey?: string; - onSearch?: (value: string) => void; +interface SearchBarProps { className?: string; } -function SearchBarComponent( - { table, searchKey, onSearch, className }: SearchBarProps, - ref: React.Ref -) { - const [searchTerm, setSearchTerm] = useState(""); - const debouncedSearchTerm = useDebounce(searchTerm, 500); +function SearchBarComponent({ className }: SearchBarProps, ref: Ref) { + const { searchTerm, setSearchTerm } = useDataTable(); + const { t } = useLocale(); + const [localValue, setLocalValue] = useState(searchTerm); - useEffect(() => { - onSearch?.(debouncedSearchTerm); - }, [debouncedSearchTerm, onSearch]); - - if (onSearch) { - return ( - setSearchTerm(event.target.value.trim())} - /> - ); - } - - if (!searchKey) { - console.error("searchKey is required if onSearch is not provided"); - return null; - } + const handleSearchChange = (event: ChangeEvent) => { + const value = event.target.value; + setLocalValue(value); + setSearchTerm(value); + }; return ( { - 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 ( - props: SearchBarProps & { ref?: React.Ref } +const SearchBar = forwardRef(SearchBarComponent) as ( + props: SearchBarProps & { ref?: React.Ref } ) => ReturnType; interface ClearFiltersButtonProps { diff --git a/packages/features/ee/teams/components/MemberList.tsx b/packages/features/ee/teams/components/MemberList.tsx index 56ceff26fa..64a81aa0f5 100644 --- a/packages/features/ee/teams/components/MemberList.tsx +++ b/packages/features/ee/teams/components/MemberList.tsx @@ -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(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)}>
- setDebouncedSearchTerm(value)} /> + {isAdminOrOwner && ( diff --git a/packages/features/settings/outOfOffice/OutOfOfficeEntriesList.tsx b/packages/features/settings/outOfOffice/OutOfOfficeEntriesList.tsx index 215f8a08e1..6bf011dc14 100644 --- a/packages/features/settings/outOfOffice/OutOfOfficeEntriesList.tsx +++ b/packages/features/settings/outOfOffice/OutOfOfficeEntriesList.tsx @@ -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(null); - const [searchTerm, setSearchTerm] = useState(""); const [deletedEntry, setDeletedEntry] = useState(0); const [currentlyEditingOutOfOfficeEntry, setCurrentlyEditingOutOfOfficeEntry] = useState(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={ <> - setSearchTerm(value)} /> + } diff --git a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx index a5bf9bdb6a..ed1d54e300 100644 --- a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx +++ b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx @@ -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(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)}> - setSearchString(value)} /> + diff --git a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx index 07e08f8b9d..b0fb371aed 100644 --- a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx +++ b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx @@ -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={ <> - setDebouncedSearchTerm(value)} - className="sm:max-w-64 max-w-full" - /> + diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index 8225f47591..12c01c3e2d 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -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={ <> - setDebouncedSearchTerm(value)} /> +