feat: implement serverside search over client side (#14234)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
co-authored by
Udit Takkar
parent
f5876ae6d6
commit
eec76ecfe2
@@ -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 (
|
||||
<>
|
||||
<DataTable
|
||||
searchKey="member"
|
||||
onSearch={(value) => setDebouncedSearchTerm(value)}
|
||||
selectionOptions={[
|
||||
{
|
||||
type: "render",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof ZListMembersSchema>;
|
||||
|
||||
@@ -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<TData> {
|
||||
filterableItems?: FilterableItems;
|
||||
searchKey?: string;
|
||||
tableCTA?: React.ReactNode;
|
||||
onSearch?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
@@ -32,10 +35,17 @@ export function DataTableToolbar<TData>({
|
||||
filterableItems,
|
||||
tableCTA,
|
||||
searchKey,
|
||||
onSearch,
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
// 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<TData>({
|
||||
onChange={(event) => table.getColumn(searchKey)?.setFilterValue(event.target.value.trim())}
|
||||
/>
|
||||
)}
|
||||
{onSearch && (
|
||||
<Input
|
||||
className="max-w-64 mb-0 mr-auto rounded-md"
|
||||
placeholder="Search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isFiltered && (
|
||||
<Button
|
||||
color="minimal"
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
searchKey?: string;
|
||||
onSearch?: (value: string) => void;
|
||||
filterableItems?: FilterableItems;
|
||||
selectionOptions?: ActionItem<TData>[];
|
||||
tableCTA?: React.ReactNode;
|
||||
@@ -53,6 +54,7 @@ export function DataTable<TData, TValue>({
|
||||
tableOverlay,
|
||||
variant,
|
||||
/** This should only really be used if you dont have actions in a row. */
|
||||
onSearch,
|
||||
onRowMouseclick,
|
||||
onScroll,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
@@ -102,6 +104,7 @@ export function DataTable<TData, TValue>({
|
||||
table={table}
|
||||
filterableItems={filterableItems}
|
||||
searchKey={searchKey}
|
||||
onSearch={onSearch}
|
||||
tableCTA={tableCTA}
|
||||
/>
|
||||
<div ref={tableContainerRef} onScroll={onScroll}>
|
||||
|
||||
Reference in New Issue
Block a user