diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 845fd65e2a..a351f9615b 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -511,9 +511,9 @@ function BookingListItem(booking: BookingItemProps) { - +
- +
{eventTypeColor && (
@@ -562,8 +562,10 @@ function BookingListItem(booking: BookingItemProps) {
- - +
+
{/* Time and Badges for mobile */}
@@ -648,8 +650,8 @@ function BookingListItem(booking: BookingItemProps) { )}
- - +
+
{isUpcoming && !isCancelled ? ( <> {isPending && } @@ -674,7 +676,7 @@ function BookingListItem(booking: BookingItemProps) {
)} - +
- + {isBookingReroutable(parsedBooking) && ( +
{isPending && ( {t("unconfirmed")} @@ -1251,11 +1253,9 @@ const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: Assig const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum); return ( {assignmentReason.reasonString}

}> -
- - {t(badgeTitle)} - -
+ + {t(badgeTitle)} +
); }; diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 7333bda725..0604814265 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -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, 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 ( + + + + ); +} + +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(); - 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 ( + + ); + }, + }), + ]; + }, [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({ + 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(); - return (
@@ -147,7 +208,7 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
-
+
{query.status === "error" && ( )} @@ -159,69 +220,41 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu

{t("today")}

- - - - {bookingsToday.map((booking: BookingOutput) => ( - - ))} - - -
+
+ + {bookingsToday.map((booking: BookingOutput) => ( + + ))} + +
)} -
-
- - - {query.data.pages.map((page, index) => ( - - {page.bookings.filter(filterBookings).map((booking: BookingOutput) => { - const recurringInfo = page.recurringInfo.find( - (info) => info.recurringEventId === booking.recurringEventId - ); - return ( - - ); - })} - - ))} - -
-
-
- -
-
+ + )} {query.status === "success" && isEmpty && ( diff --git a/packages/features/bookings/components/PeopleFilter.tsx b/packages/features/bookings/components/PeopleFilter.tsx index dca96cdf1b..5435ca38a6 100644 --- a/packages/features/bookings/components/PeopleFilter.tsx +++ b/packages/features/bookings/components/PeopleFilter.tsx @@ -54,6 +54,8 @@ export const PeopleFilter = () => { return ( + setSearchText(e.target.value)} placeholder={t("search")} /> + } @@ -62,7 +64,6 @@ export const PeopleFilter = () => { label={t("all_users_filter_label")} /> - setSearchText(e.target.value)} placeholder={t("search")} /> {filteredMembers?.map((member) => ( = { onScroll?: (e: Pick, "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({ onRowMouseclick, onScroll, children, + hideHeader, identifier: _identifier, enableColumnResizing, + testId, + bodyTestId, ...rest }: DataTableProps & React.ComponentPropsWithoutRef<"div">) { const pathname = usePathname() as string | null; @@ -89,7 +94,7 @@ export function DataTable({ gridTemplateAreas: "'header' 'body' 'footer'", ...rest.style, }} - data-testid={rest["data-testid"] ?? "data-table"}> + data-testid={testId ?? "data-table"}>
({ ...columnSizingVars, ...(Boolean(enableColumnResizing) && { width: table.getTotalSize() }), }}> - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const meta = header.column.columnDef.meta; - return ( - -
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - {header.column.getIsSorted() && ( - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta; + return ( + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getIsSorted() && ( + + )} +
+ {Boolean(enableColumnResizing) && header.column.getCanResize() && ( +
)} -
- {Boolean(enableColumnResizing) && header.column.getCanResize() && ( -
- )} - - ); - })} - - ))} - + + ); + })} + + ))} + + )} {/* When resizing any column we will render this special memoized version of our table body */} {table.getState().columnSizingInfo.isResizingColumn ? ( ({ 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 = { table: ReactTableType; rowVirtualizer: Virtualizer; rows: Row[]; + testId?: string; variant?: "default" | "compact"; isPending?: boolean; onRowMouseclick?: (row: Row) => void; @@ -209,13 +220,17 @@ function DataTableBody({ table, rowVirtualizer, rows, + testId, variant, isPending, onRowMouseclick, }: DataTableBodyProps) { const virtualRows = rowVirtualizer.getVirtualItems(); return ( - + {virtualRows && !isPending ? ( virtualRows.map((virtualRow) => { const row = rows[virtualRow.index] as Row; @@ -232,11 +247,7 @@ function DataTableBody({ 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({ }} 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" )}> diff --git a/packages/features/data-table/components/DataTableWrapper.tsx b/packages/features/data-table/components/DataTableWrapper.tsx index 52df35f0e6..f6ff968e4f 100644 --- a/packages/features/data-table/components/DataTableWrapper.tsx +++ b/packages/features/data-table/components/DataTableWrapper.tsx @@ -12,25 +12,31 @@ import { export type DataTableWrapperProps = { testId?: string; + bodyTestId?: string; table: ReactTableType; 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({ testId, + bodyTestId, table, isPending, hasNextPage, fetchNextPage, isFetching, totalDBRowCount, + variant, + hideHeader, ToolbarLeft, ToolbarRight, children, @@ -45,11 +51,14 @@ export function DataTableWrapper({ return ( fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
diff --git a/packages/features/ee/teams/components/MemberList.tsx b/packages/features/ee/teams/components/MemberList.tsx index 1380c3ee09..6b01f8bea4 100644 --- a/packages/features/ee/teams/components/MemberList.tsx +++ b/packages/features/ee/teams/components/MemberList.tsx @@ -659,7 +659,7 @@ function MemberListContent(props: Props) { return ( <> { 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")}: `}> + setSearch(e.target.value)} + /> + } @@ -98,6 +113,7 @@ export const TeamsFilter = ({ {teams ?.filter((team) => !team?.isOrganization) + .filter((team) => team.name.toLowerCase().includes(search.toLowerCase())) .map((team) => (