diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index a351f9615b..3ae610aad9 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -68,6 +68,7 @@ type BookingItemProps = BookingItem & { userTimeFormat: number | null | undefined; userEmail: string | undefined; }; + isToday: boolean; }; type ParsedBooking = ReturnType; @@ -511,7 +512,10 @@ function BookingListItem(booking: BookingItemProps) { -
+
diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 06626e4294..36e4aaba69 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -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 ( - - ); + if (props.row.original.type === "data") { + const { booking, recurringInfo, isToday } = props.row.original; + return ( + + ); + } else if (props.row.original.type === "today") { + return ( +

+ {t("today")} +

+ ); + } else if (props.row.original.type === "next") { + return ( +

+ {t("next")} +

+ ); + } }, }), ]; }, [user, status]); - const isEmpty = !query.data?.pages[0]?.bookings.length; + const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]); - const shownBookings: Record = {}; - const filterBookings = (booking: BookingOutput) => { - if (status === "recurring" || status == "unconfirmed" || status === "cancelled") { - if (!booking.recurringEventId) { - return true; + const flatData = useMemo(() => { + const shownBookings: Record = {}; + 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(() => { + 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(() => { + 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({ - 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 (
@@ -216,48 +258,19 @@ function BookingsContent({ status }: BookingsProps) { {query.status === "success" && !isEmpty && ( <> {!!bookingsToday.length && status === "upcoming" && ( -
- -

{t("today")}

-
-
- - {bookingsToday.map((booking: BookingOutput) => ( - - ))} - -
-
-
- )} - - {flatData.length > 0 && ( - + )} + )} {query.status === "success" && isEmpty && ( diff --git a/apps/web/playwright/wipe-my-cal.e2e.ts b/apps/web/playwright/wipe-my-cal.e2e.ts index 00c754f85c..a526a1e2f3 100644 --- a/apps/web/playwright/wipe-my-cal.e2e.ts +++ b/apps/web/playwright/wipe-my-cal.e2e.ts @@ -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(); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 51958ad442..430d3aee89 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/data-table/components/DataTableWrapper.tsx b/packages/features/data-table/components/DataTableWrapper.tsx index 3ef7c032e6..4306bab17e 100644 --- a/packages/features/data-table/components/DataTableWrapper.tsx +++ b/packages/features/data-table/components/DataTableWrapper.tsx @@ -66,16 +66,18 @@ export function DataTableWrapper({ className={className} containerClassName={containerClassName} onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}> - -
-
-
{ToolbarLeft}
-
{ToolbarRight}
+ {(ToolbarLeft || ToolbarRight) && ( + +
+
+
{ToolbarLeft}
+
{ToolbarRight}
+
-
- {children} - + {children} + + )} {totalDBRowCount && (