fix: nested scrolls on /bookings/upcoming (#18760)

This commit is contained in:
Eunjae Lee
2025-01-20 09:18:12 -05:00
committed by GitHub
parent bfeafaa21f
commit 5357228f72
5 changed files with 133 additions and 113 deletions
@@ -68,6 +68,7 @@ type BookingItemProps = BookingItem & {
userTimeFormat: number | null | undefined;
userEmail: string | undefined;
};
isToday: boolean;
};
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
@@ -511,7 +512,10 @@ function BookingListItem(booking: BookingItemProps) {
</DialogFooter>
</DialogContent>
</Dialog>
<div data-testid="booking-item" className="hover:bg-muted group w-full">
<div
data-testid="booking-item"
data-today={String(booking.isToday)}
className="hover:bg-muted group w-full">
<div className="flex flex-col sm:flex-row">
<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">
@@ -8,7 +8,7 @@ import {
createColumnHelper,
} from "@tanstack/react-table";
import type { ReactElement } from "react";
import { Fragment, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import type { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
@@ -85,10 +85,16 @@ export default function Bookings(props: BookingsProps) {
);
}
type RowData = {
booking: BookingOutput;
recurringInfo?: RecurringInfo;
};
type RowData =
| {
type: "data";
booking: BookingOutput;
isToday: boolean;
recurringInfo?: RecurringInfo;
}
| {
type: "today" | "next";
};
function BookingsContent({ status }: BookingsProps) {
const { data: filterQuery } = useFilterQuery();
@@ -118,88 +124,124 @@ function BookingsContent({ status }: BookingsProps) {
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}
/>
);
if (props.row.original.type === "data") {
const { booking, recurringInfo, isToday } = props.row.original;
return (
<BookingListItem
key={booking.id}
isToday={isToday}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
} else if (props.row.original.type === "today") {
return (
<p className="text-subtle bg-subtle w-full py-4 pl-6 text-xs font-semibold uppercase leading-4">
{t("today")}
</p>
);
} else if (props.row.original.type === "next") {
return (
<p className="text-subtle bg-subtle w-full py-4 pl-6 text-xs font-semibold uppercase leading-4">
{t("next")}
</p>
);
}
},
}),
];
}, [user, status]);
const isEmpty = !query.data?.pages[0]?.bookings.length;
const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]);
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
const flatData = useMemo<RowData[]>(() => {
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
if (
shownBookings[booking.recurringEventId] !== undefined &&
shownBookings[booking.recurringEventId].length > 0
) {
shownBookings[booking.recurringEventId].push(booking);
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
}
if (
shownBookings[booking.recurringEventId] !== undefined &&
shownBookings[booking.recurringEventId].length > 0
) {
shownBookings[booking.recurringEventId].push(booking);
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
}
return true;
};
return true;
};
const flatData = useMemo(() => {
return (
query.data?.pages.flatMap((page) =>
page.bookings.filter(filterBookings).map((booking) => ({
type: "data",
booking,
recurringInfo: page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
),
isToday: false,
}))
) || []
);
}, [query.data]);
const bookingsToday = useMemo<RowData[]>(() => {
return (
query.data?.pages.map((page) =>
page.bookings
.filter(
(booking: BookingOutput) =>
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
)
.map((booking) => ({
type: "data" as const,
booking,
recurringInfo: page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
),
isToday: true,
}))
)[0] || []
);
}, [query.data]);
const finalData = useMemo<RowData[]>(() => {
if (bookingsToday.length > 0 && status === "upcoming") {
const merged: RowData[] = [
{ type: "today" as const },
...bookingsToday,
{ type: "next" as const },
...flatData,
];
return merged;
} else {
return flatData;
}
}, [bookingsToday, flatData, status]);
const table = useReactTable<RowData>({
data: flatData,
data: finalData,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
});
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
})
)[0] || [];
return (
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
@@ -216,48 +258,19 @@ function BookingsContent({ status }: BookingsProps) {
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<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">
<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>
)}
{flatData.length > 0 && (
<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"
containerClassName="!h-[inherit] max-h-[80dvh]"
/>
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
)}
<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 && (
+3 -3
View File
@@ -42,9 +42,9 @@ test.describe("Wipe my Cal App Test", () => {
await page.goto("/bookings/upcoming");
await expect(page.locator("data-testid=wipe-today-button")).toBeVisible();
const $openBookingCount = await page.locator('[data-testid="bookings"] > *').count();
const $todayBookingCount = await page.locator('[data-testid="today-bookings"] > *').count();
expect($openBookingCount + $todayBookingCount).toBe(3);
const $nonTodayBookingCount = await page.locator('[data-today="false"]').count();
const $todayBookingCount = await page.locator('[data-today="true"]').count();
expect($nonTodayBookingCount + $todayBookingCount).toBe(3);
await page.locator("data-testid=wipe-today-button").click();
@@ -2927,5 +2927,6 @@
"managed_users_description": "See all the managed users created by your OAuth client",
"select_oAuth_client": "Select Oauth Client",
"on_every_instance": "On every instance",
"next": "Next",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -66,16 +66,18 @@ export function DataTableWrapper<TData, TValue>({
className={className}
containerClassName={containerClassName}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
<DataTableToolbar.Root>
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap gap-2">{ToolbarLeft}</div>
<div className="flex flex-wrap gap-2">{ToolbarRight}</div>
{(ToolbarLeft || ToolbarRight) && (
<DataTableToolbar.Root>
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap gap-2">{ToolbarLeft}</div>
<div className="flex flex-wrap gap-2">{ToolbarRight}</div>
</div>
</div>
</div>
{children}
</DataTableToolbar.Root>
{children}
</DataTableToolbar.Root>
)}
{totalDBRowCount && (
<div style={{ gridArea: "footer", marginTop: "1rem" }}>