refactor: use DataTable for /bookings (#18589)
* refactor: use DataTable for /bookings * remove unnecessary animation (for better scroll performance) * add consistent search box
This commit is contained in:
1 parent
cce6e83578
commit
4f494d5a32
8 files changed
+226
-156
No files matched your search
@@ -511,9 +511,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<tr data-testid="booking-item" className="hover:bg-muted group transition ">
|
||||
<div data-testid="booking-item" className="hover:bg-muted group w-full">
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
<td className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
|
||||
<div className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
|
||||
<div className="flex h-full items-center">
|
||||
{eventTypeColor && (
|
||||
<div className="h-[70%] w-0.5" style={{ backgroundColor: eventTypeColor }} />
|
||||
@@ -562,8 +562,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||
</div>
|
||||
<div
|
||||
data-testid="title-and-attendees"
|
||||
className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||
<Link href={bookingLink}>
|
||||
{/* Time and Badges for mobile */}
|
||||
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||
@@ -648,8 +650,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
|
||||
</div>
|
||||
<div className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{isPending && <TableActions actions={pendingActions} />}
|
||||
@@ -674,7 +676,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<TableActions actions={chargeCardActions} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
<BookingItemBadges
|
||||
booking={booking}
|
||||
@@ -683,7 +685,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
userTimeFormat={userTimeFormat}
|
||||
userTimeZone={userTimeZone}
|
||||
/>
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
{isBookingReroutable(parsedBooking) && (
|
||||
<RerouteDialog
|
||||
@@ -712,7 +714,7 @@ const BookingItemBadges = ({
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="hidden h-9 flex-row pb-4 pl-6 sm:flex">
|
||||
<div className="hidden h-9 flex-row items-center pb-4 pl-6 sm:flex">
|
||||
{isPending && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("unconfirmed")}
|
||||
@@ -1251,11 +1253,9 @@ const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: Assig
|
||||
const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum);
|
||||
return (
|
||||
<Tooltip content={<p>{assignmentReason.reasonString}</p>}>
|
||||
<div className="-mt-1">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{t(badgeTitle)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{t(badgeTitle)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table";
|
||||
import type { ReactElement } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||
@@ -11,13 +17,13 @@ import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle"
|
||||
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
|
||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
|
||||
import { Alert, EmptyScreen, HorizontalTabs } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
@@ -67,7 +73,24 @@ const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
|
||||
unconfirmed: "unconfirmed_bookings",
|
||||
};
|
||||
|
||||
export default function Bookings({ status }: { status: (typeof validStatuses)[number] }) {
|
||||
type BookingsProps = {
|
||||
status: (typeof validStatuses)[number];
|
||||
};
|
||||
|
||||
export default function Bookings(props: BookingsProps) {
|
||||
return (
|
||||
<DataTableProvider>
|
||||
<BookingsContent {...props} />
|
||||
</DataTableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type RowData = {
|
||||
booking: BookingOutput;
|
||||
recurringInfo?: RecurringInfo;
|
||||
};
|
||||
|
||||
function BookingsContent({ status }: BookingsProps) {
|
||||
const { data: filterQuery } = useFilterQuery();
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -88,13 +111,32 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
}
|
||||
);
|
||||
|
||||
// Animate page (tab) transitions to look smoothing
|
||||
const columns = useMemo(() => {
|
||||
const columnHelper = createColumnHelper<RowData>();
|
||||
|
||||
const buttonInView = useInViewObserver(() => {
|
||||
if (!query.isFetching && query.hasNextPage && query.status === "success") {
|
||||
query.fetchNextPage();
|
||||
}
|
||||
});
|
||||
return [
|
||||
columnHelper.display({
|
||||
id: "custom-view",
|
||||
cell: (props) => {
|
||||
const { booking, recurringInfo } = props.row.original;
|
||||
return (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfo}
|
||||
{...booking}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
}, [user, status]);
|
||||
|
||||
const isEmpty = !query.data?.pages[0]?.bookings.length;
|
||||
|
||||
@@ -121,6 +163,27 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
return true;
|
||||
};
|
||||
|
||||
const flatData = useMemo(() => {
|
||||
return (
|
||||
query.data?.pages.flatMap((page) =>
|
||||
page.bookings.filter(filterBookings).map((booking) => ({
|
||||
booking,
|
||||
recurringInfo: page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
),
|
||||
}))
|
||||
) || []
|
||||
);
|
||||
}, [query.data]);
|
||||
|
||||
const table = useReactTable<RowData>({
|
||||
data: flatData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
});
|
||||
|
||||
let recurringInfoToday: RecurringInfo | undefined;
|
||||
|
||||
const bookingsToday =
|
||||
@@ -137,8 +200,6 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
})
|
||||
)[0] || [];
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
@@ -147,7 +208,7 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
</div>
|
||||
<FiltersContainer isFiltersVisible={isFiltersVisible} />
|
||||
<main className="w-full">
|
||||
<div className="flex w-full flex-col" ref={animationParentRef}>
|
||||
<div className="flex w-full flex-col">
|
||||
{query.status === "error" && (
|
||||
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
||||
)}
|
||||
@@ -159,69 +220,41 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
|
||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
|
||||
<Fragment>
|
||||
{bookingsToday.map((booking: BookingOutput) => (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfoToday}
|
||||
{...booking}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
className="bg-default divide-subtle w-full max-w-full divide-y"
|
||||
data-testid="today-bookings">
|
||||
<Fragment>
|
||||
{bookingsToday.map((booking: BookingOutput) => (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfoToday}
|
||||
{...booking}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2 xl:pt-0">
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
||||
{query.data.pages.map((page, index) => (
|
||||
<Fragment key={index}>
|
||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
||||
const recurringInfo = page.recurringInfo.find(
|
||||
(info) => info.recurringEventId === booking.recurringEventId
|
||||
);
|
||||
return (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
loggedInUser={{
|
||||
userId: user?.id,
|
||||
userTimeZone: user?.timeZone,
|
||||
userTimeFormat: user?.timeFormat,
|
||||
userEmail: user?.email,
|
||||
}}
|
||||
listingStatus={status}
|
||||
recurringInfo={recurringInfo}
|
||||
{...booking}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
|
||||
<Button
|
||||
color="minimal"
|
||||
loading={query.isFetchingNextPage}
|
||||
disabled={!query.hasNextPage}
|
||||
onClick={() => query.fetchNextPage()}>
|
||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTableWrapper
|
||||
table={table}
|
||||
testId={`${status}-bookings`}
|
||||
bodyTestId="bookings"
|
||||
hideHeader={true}
|
||||
isPending={query.isFetching && !flatData}
|
||||
hasNextPage={query.hasNextPage}
|
||||
fetchNextPage={query.fetchNextPage}
|
||||
isFetching={query.isFetching}
|
||||
variant="compact"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{query.status === "success" && isEmpty && (
|
||||
|
||||
@@ -54,6 +54,8 @@ export const PeopleFilter = () => {
|
||||
return (
|
||||
<AnimatedPopover text={getTextForPopover()} prefix={`${t("people")}: `}>
|
||||
<FilterCheckboxFieldsContainer>
|
||||
<FilterSearchField onChange={(e) => setSearchText(e.target.value)} placeholder={t("search")} />
|
||||
|
||||
<FilterCheckboxField
|
||||
id="all"
|
||||
icon={<Icon name="user" className="h-4 w-4" />}
|
||||
@@ -62,7 +64,6 @@ export const PeopleFilter = () => {
|
||||
label={t("all_users_filter_label")}
|
||||
/>
|
||||
<Divider />
|
||||
<FilterSearchField onChange={(e) => setSearchText(e.target.value)} placeholder={t("search")} />
|
||||
|
||||
{filteredMembers?.map((member) => (
|
||||
<FilterCheckboxField
|
||||
|
||||
@@ -23,7 +23,9 @@ export type DataTableProps<TData, TValue> = {
|
||||
onScroll?: (e: Pick<React.UIEvent<HTMLDivElement, UIEvent>, "target">) => void;
|
||||
tableOverlay?: React.ReactNode;
|
||||
variant?: "default" | "compact";
|
||||
"data-testid"?: string;
|
||||
testId?: string;
|
||||
bodyTestId?: string;
|
||||
hideHeader?: boolean;
|
||||
children?: React.ReactNode;
|
||||
identifier?: string;
|
||||
enableColumnResizing?: boolean;
|
||||
@@ -37,8 +39,11 @@ export function DataTable<TData, TValue>({
|
||||
onRowMouseclick,
|
||||
onScroll,
|
||||
children,
|
||||
hideHeader,
|
||||
identifier: _identifier,
|
||||
enableColumnResizing,
|
||||
testId,
|
||||
bodyTestId,
|
||||
...rest
|
||||
}: DataTableProps<TData, TValue> & React.ComponentPropsWithoutRef<"div">) {
|
||||
const pathname = usePathname() as string | null;
|
||||
@@ -89,7 +94,7 @@ export function DataTable<TData, TValue>({
|
||||
gridTemplateAreas: "'header' 'body' 'footer'",
|
||||
...rest.style,
|
||||
}}
|
||||
data-testid={rest["data-testid"] ?? "data-table"}>
|
||||
data-testid={testId ?? "data-table"}>
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
onScroll={onScroll}
|
||||
@@ -104,66 +109,69 @@ export function DataTable<TData, TValue>({
|
||||
...columnSizingVars,
|
||||
...(Boolean(enableColumnResizing) && { width: table.getTotalSize() }),
|
||||
}}>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full">
|
||||
{headerGroup.headers.map((header) => {
|
||||
const meta = header.column.columnDef.meta;
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{
|
||||
...(meta?.sticky?.position === "left" && { left: `${meta.sticky.gap || 0}px` }),
|
||||
...(meta?.sticky?.position === "right" && { right: `${meta.sticky.gap || 0}px` }),
|
||||
width: `var(--header-${kebabCase(header?.id)}-size)`,
|
||||
}}
|
||||
className={classNames(
|
||||
"relative flex shrink-0 items-center",
|
||||
header.column.getCanSort()
|
||||
? "bg-subtle hover:bg-muted cursor-pointer select-none"
|
||||
: "",
|
||||
meta?.sticky && "top-0 z-20 sm:sticky"
|
||||
)}>
|
||||
<div
|
||||
className="flex h-full w-full items-center overflow-hidden"
|
||||
onClick={header.column.getToggleSortingHandler()}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() && (
|
||||
<Icon
|
||||
name="arrow-up"
|
||||
className="ml-2 h-4 w-4"
|
||||
style={{
|
||||
transform:
|
||||
header.column.getIsSorted() === "asc" ? "rotate(0deg)" : "rotate(180deg)",
|
||||
transition: "transform 0.2s ease-in-out",
|
||||
}}
|
||||
{!hideHeader && (
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full">
|
||||
{headerGroup.headers.map((header) => {
|
||||
const meta = header.column.columnDef.meta;
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{
|
||||
...(meta?.sticky?.position === "left" && { left: `${meta.sticky.gap || 0}px` }),
|
||||
...(meta?.sticky?.position === "right" && { right: `${meta.sticky.gap || 0}px` }),
|
||||
width: `var(--header-${kebabCase(header?.id)}-size)`,
|
||||
}}
|
||||
className={classNames(
|
||||
"relative flex shrink-0 items-center",
|
||||
header.column.getCanSort()
|
||||
? "bg-subtle hover:bg-muted cursor-pointer select-none"
|
||||
: "",
|
||||
meta?.sticky && "top-0 z-20 sm:sticky"
|
||||
)}>
|
||||
<div
|
||||
className="flex h-full w-full items-center overflow-hidden"
|
||||
onClick={header.column.getToggleSortingHandler()}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() && (
|
||||
<Icon
|
||||
name="arrow-up"
|
||||
className="ml-2 h-4 w-4"
|
||||
style={{
|
||||
transform:
|
||||
header.column.getIsSorted() === "asc" ? "rotate(0deg)" : "rotate(180deg)",
|
||||
transition: "transform 0.2s ease-in-out",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(enableColumnResizing) && header.column.getCanResize() && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={classNames(
|
||||
"bg-inverted absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
|
||||
header.column.getIsResizing() && "!opacity-75"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(enableColumnResizing) && header.column.getCanResize() && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={classNames(
|
||||
"bg-inverted absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
|
||||
header.column.getIsResizing() && "!opacity-75"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
)}
|
||||
{/* When resizing any column we will render this special memoized version of our table body */}
|
||||
{table.getState().columnSizingInfo.isResizingColumn ? (
|
||||
<MemoizedTableBody
|
||||
table={table}
|
||||
rowVirtualizer={rowVirtualizer}
|
||||
rows={rows}
|
||||
testId={bodyTestId}
|
||||
variant={variant}
|
||||
isPending={isPending}
|
||||
onRowMouseclick={onRowMouseclick}
|
||||
@@ -173,6 +181,7 @@ export function DataTable<TData, TValue>({
|
||||
table={table}
|
||||
rowVirtualizer={rowVirtualizer}
|
||||
rows={rows}
|
||||
testId={bodyTestId}
|
||||
variant={variant}
|
||||
isPending={isPending}
|
||||
onRowMouseclick={onRowMouseclick}
|
||||
@@ -191,6 +200,7 @@ const MemoizedTableBody = memo(
|
||||
prev.table.options.data === next.table.options.data &&
|
||||
prev.rowVirtualizer === next.rowVirtualizer &&
|
||||
prev.rows === next.rows &&
|
||||
prev.testId === next.testId &&
|
||||
prev.variant === next.variant &&
|
||||
prev.isPending === next.isPending &&
|
||||
prev.onRowMouseclick === next.onRowMouseclick
|
||||
@@ -200,6 +210,7 @@ type DataTableBodyProps<TData> = {
|
||||
table: ReactTableType<TData>;
|
||||
rowVirtualizer: Virtualizer<HTMLDivElement, Element>;
|
||||
rows: Row<TData>[];
|
||||
testId?: string;
|
||||
variant?: "default" | "compact";
|
||||
isPending?: boolean;
|
||||
onRowMouseclick?: (row: Row<TData>) => void;
|
||||
@@ -209,13 +220,17 @@ function DataTableBody<TData>({
|
||||
table,
|
||||
rowVirtualizer,
|
||||
rows,
|
||||
testId,
|
||||
variant,
|
||||
isPending,
|
||||
onRowMouseclick,
|
||||
}: DataTableBodyProps<TData>) {
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
return (
|
||||
<TableBody className="relative grid" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
|
||||
<TableBody
|
||||
className="relative grid"
|
||||
data-testid={testId}
|
||||
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
|
||||
{virtualRows && !isPending ? (
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<TData>;
|
||||
@@ -232,11 +247,7 @@ function DataTableBody<TData>({
|
||||
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
|
||||
width: "100%",
|
||||
}}
|
||||
className={classNames(
|
||||
onRowMouseclick && "hover:cursor-pointer",
|
||||
variant === "compact" && "!border-0",
|
||||
"group"
|
||||
)}>
|
||||
className={classNames(onRowMouseclick && "hover:cursor-pointer", "group")}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const column = table.getColumn(cell.column.id);
|
||||
const meta = column?.columnDef.meta;
|
||||
@@ -250,7 +261,7 @@ function DataTableBody<TData>({
|
||||
}}
|
||||
className={classNames(
|
||||
"flex shrink-0 items-center overflow-hidden",
|
||||
variant === "compact" && "p-1.5",
|
||||
variant === "compact" && "p-0",
|
||||
meta?.sticky &&
|
||||
"bg-default group-hover:!bg-muted group-data-[state=selected]:bg-subtle sm:sticky"
|
||||
)}>
|
||||
|
||||
@@ -12,25 +12,31 @@ import {
|
||||
|
||||
export type DataTableWrapperProps<TData, TValue> = {
|
||||
testId?: string;
|
||||
bodyTestId?: string;
|
||||
table: ReactTableType<TData>;
|
||||
isPending: boolean;
|
||||
hasNextPage: boolean;
|
||||
fetchNextPage: () => void;
|
||||
isFetching: boolean;
|
||||
hideHeader?: boolean;
|
||||
variant?: "default" | "compact";
|
||||
totalDBRowCount?: number;
|
||||
ToolbarLeft?: React.ReactNode;
|
||||
ToolbarRight?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function DataTableWrapper<TData, TValue>({
|
||||
testId,
|
||||
bodyTestId,
|
||||
table,
|
||||
isPending,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetching,
|
||||
totalDBRowCount,
|
||||
variant,
|
||||
hideHeader,
|
||||
ToolbarLeft,
|
||||
ToolbarRight,
|
||||
children,
|
||||
@@ -45,11 +51,14 @@ export function DataTableWrapper<TData, TValue>({
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data-testid={testId}
|
||||
testId={testId}
|
||||
bodyTestId={bodyTestId}
|
||||
table={table}
|
||||
tableContainerRef={tableContainerRef}
|
||||
isPending={isPending}
|
||||
enableColumnResizing={true}
|
||||
hideHeader={hideHeader}
|
||||
variant={variant}
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
|
||||
<DataTableToolbar.Root>
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row">
|
||||
|
||||
@@ -659,7 +659,7 @@ function MemberListContent(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data-testid="team-member-list-container"
|
||||
testId="team-member-list-container"
|
||||
table={table}
|
||||
tableContainerRef={tableContainerRef}
|
||||
isPending={isPending}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { InputHTMLAttributes, ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useState } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
@@ -8,7 +8,15 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { AnimatedPopover, Avatar, Divider, Icon, Tooltip, VerticalDivider } from "@calcom/ui";
|
||||
import {
|
||||
FilterSearchField,
|
||||
AnimatedPopover,
|
||||
Avatar,
|
||||
Divider,
|
||||
Icon,
|
||||
Tooltip,
|
||||
VerticalDivider,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
|
||||
|
||||
@@ -31,6 +39,7 @@ export const TeamsFilter = ({
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
|
||||
|
||||
@@ -72,6 +81,12 @@ export const TeamsFilter = ({
|
||||
popoverTriggerClassNames={popoverTriggerClassNames}
|
||||
prefix={`${t("teams")}: `}>
|
||||
<FilterCheckboxFieldsContainer>
|
||||
<FilterSearchField
|
||||
placeholder={t("search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<FilterCheckboxField
|
||||
id="all"
|
||||
icon={<Icon name="layers" className="h-4 w-4" />}
|
||||
@@ -98,6 +113,7 @@ export const TeamsFilter = ({
|
||||
<Divider />
|
||||
{teams
|
||||
?.filter((team) => !team?.isOrganization)
|
||||
.filter((team) => team.name.toLowerCase().includes(search.toLowerCase()))
|
||||
.map((team) => (
|
||||
<FilterCheckboxField
|
||||
key={team.id}
|
||||
|
||||
@@ -311,7 +311,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps)
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data-testid="managed-user-list-data-table"
|
||||
testId="managed-user-list-data-table"
|
||||
table={table}
|
||||
tableContainerRef={tableContainerRef}
|
||||
isPending={isPending}
|
||||
|
||||
Reference in new issue
Block a user