diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index 5208be795a..f9b4b46641 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -2,7 +2,7 @@ import { keepPreviousData } from "@tanstack/react-query"; import type { ColumnDef } from "@tanstack/react-table"; import { Plus } from "lucide-react"; import { useSession } from "next-auth/react"; -import { useCallback, useEffect, useMemo, useReducer, useRef } from "react"; +import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -115,11 +115,12 @@ export function UserListTable() { const [state, dispatch] = useReducer(reducer, initialState); const { t } = useLocale(); const orgBranding = useOrgBranding(); - + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const { data, isPending, fetchNextPage, isFetching } = trpc.viewer.organizations.listMembers.useInfiniteQuery( { limit: 10, + searchTerm: debouncedSearchTerm, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, @@ -310,7 +311,7 @@ export function UserListTable() { return ( <> setDebouncedSearchTerm(value)} selectionOptions={[ { type: "render", diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts index 679ee8f38b..bedc8234df 100644 --- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts @@ -15,6 +15,7 @@ type GetOptions = { export const listMembersHandler = async ({ ctx, input }: GetOptions) => { const organizationId = ctx.user.organizationId; + const searchTerm = input.searchTerm; if (!organizationId) { throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." }); @@ -42,6 +43,22 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { const teamMembers = await prisma.membership.findMany({ where: { teamId: organizationId, + ...(searchTerm && { + user: { + OR: [ + { + email: { + contains: searchTerm, + }, + }, + { + username: { + contains: searchTerm, + }, + }, + ], + }, + }), }, select: { id: true, diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts index 4f07429566..7118e83e0d 100644 --- a/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts +++ b/packages/trpc/server/routers/viewer/organizations/listMembers.schema.ts @@ -3,6 +3,7 @@ import { z } from "zod"; export const ZListMembersSchema = z.object({ limit: z.number().min(1).max(100), cursor: z.number().nullish(), + searchTerm: z.string().optional(), }); export type TListMembersSchema = z.infer; diff --git a/packages/ui/components/data-table/DataTableToolbar.tsx b/packages/ui/components/data-table/DataTableToolbar.tsx index e5d6f6e9c5..861ec3f24e 100644 --- a/packages/ui/components/data-table/DataTableToolbar.tsx +++ b/packages/ui/components/data-table/DataTableToolbar.tsx @@ -3,7 +3,9 @@ import type { Table } from "@tanstack/react-table"; import type { LucideIcon } from "lucide-react"; import { X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button } from "../button"; @@ -25,6 +27,7 @@ interface DataTableToolbarProps { filterableItems?: FilterableItems; searchKey?: string; tableCTA?: React.ReactNode; + onSearch?: (value: string) => void; } export function DataTableToolbar({ @@ -32,10 +35,17 @@ export function DataTableToolbar({ filterableItems, tableCTA, searchKey, + onSearch, }: DataTableToolbarProps) { // TODO: Is there a better way to check if the table is filtered? // If you select ALL filters for a column, the table is not filtered and we dont get a reset button const isFiltered = table.getState().columnFilters.length > 0; + const [searchTerm, setSearchTerm] = useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + useEffect(() => { + onSearch?.(debouncedSearchTerm); + }, [debouncedSearchTerm, onSearch]); const { t } = useLocale(); @@ -49,6 +59,16 @@ export function DataTableToolbar({ onChange={(event) => table.getColumn(searchKey)?.setFilterValue(event.target.value.trim())} /> )} + {onSearch && ( + { + setSearchTerm(e.target.value); + }} + /> + )} {isFiltered && (