From fddee718297e9f04f37f2f6824a366f13ea2a5eb Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 21 Oct 2025 13:09:47 +0200 Subject: [PATCH] refactor: extract bookings list and calendar views (#24486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: extract bookings list and calendar views with nuqs state management - Extract list-related code into BookingsListView component - Create empty BookingsCalendarView component for future implementation - Add nuqs query param state management for view toggle (defaults to list) - Update bookings-listing-view to conditionally render views - No visible changes to users (list view remains default) Co-Authored-By: eunjae@cal.com * refactor: move data fetching logic to parent component - Keep useFilterValue calls, trpc query, columns, flatData, bookingsToday, finalData, and table setup in parent component - BookingsListView now receives data as props instead of fetching it - This allows both list and calendar views to share the same data source Co-Authored-By: eunjae@cal.com * fix: add customView column back to render booking items The customView column was inadvertently removed during refactoring. This column is crucial as it renders the actual BookingListItem components, the "today" header, and the "next" header for the bookings list. Co-Authored-By: eunjae@cal.com * refactor: rename files for better clarity - Renamed bookings-listing-view.tsx → bookings-view.tsx (parent view) - Renamed bookings-list-view.tsx → BookingsList.tsx (list component) - Renamed bookings-calendar-view.tsx → BookingsCalendar.tsx (calendar component) - Moved list and calendar components from views/ to components/ directory - Updated all imports to reflect new structure This creates a clearer hierarchy where -view is the orchestrator and components are the renderers. Co-Authored-By: eunjae@cal.com * clean up implementation * clean up types * revert unnecessary changes * Update packages/features/data-table/GUIDE.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .agents/knowledge-base.md | 2 +- .../(main-nav)/bookings/[status]/page.tsx | 2 +- .../bookings/components/BookingsCalendar.tsx | 34 ++++++ .../bookings/components/BookingsList.tsx | 69 ++++++++++++ apps/web/modules/bookings/types.ts | 25 +++++ ...ngs-listing-view.tsx => bookings-view.tsx} | 102 +++++------------- packages/features/data-table/GUIDE.md | 2 +- 7 files changed, 159 insertions(+), 77 deletions(-) create mode 100644 apps/web/modules/bookings/components/BookingsCalendar.tsx create mode 100644 apps/web/modules/bookings/components/BookingsList.tsx create mode 100644 apps/web/modules/bookings/types.ts rename apps/web/modules/bookings/views/{bookings-listing-view.tsx => bookings-view.tsx} (82%) diff --git a/.agents/knowledge-base.md b/.agents/knowledge-base.md index ce55849012..36d25bc53f 100644 --- a/.agents/knowledge-base.md +++ b/.agents/knowledge-base.md @@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/ Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency: - Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx` -- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx` +- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx` - Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views ## When working on workflow triggers or similar enum-based features in the Cal.com codebase diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx index 21379a8a6f..dfb17af3ee 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx @@ -12,7 +12,7 @@ import { MembershipRole } from "@calcom/prisma/enums"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import { validStatuses } from "~/bookings/lib/validStatuses"; -import BookingsList from "~/bookings/views/bookings-listing-view"; +import BookingsList from "~/bookings/views/bookings-view"; const querySchema = z.object({ status: z.enum(validStatuses), diff --git a/apps/web/modules/bookings/components/BookingsCalendar.tsx b/apps/web/modules/bookings/components/BookingsCalendar.tsx new file mode 100644 index 0000000000..eb1cf6b6c5 --- /dev/null +++ b/apps/web/modules/bookings/components/BookingsCalendar.tsx @@ -0,0 +1,34 @@ +"use client"; + +import type { Table as ReactTable } from "@tanstack/react-table"; + +import { DataTableFilters, DataTableSegment } from "@calcom/features/data-table"; +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +import type { RowData, BookingListingStatus } from "../types"; + +type BookingsCalendarViewProps = { + status: BookingListingStatus; + table: ReactTable; +}; + +export function BookingsCalendar({ table }: BookingsCalendarViewProps) { + return ( + <> +
+
+ +
+ +
+ + + +
+
+
+ +
+ + ); +} diff --git a/apps/web/modules/bookings/components/BookingsList.tsx b/apps/web/modules/bookings/components/BookingsList.tsx new file mode 100644 index 0000000000..ae4ad90067 --- /dev/null +++ b/apps/web/modules/bookings/components/BookingsList.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { Table as ReactTable } from "@tanstack/react-table"; + +import { DataTableWrapper, DataTableFilters, DataTableSegment } from "@calcom/features/data-table"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +import SkeletonLoader from "@components/booking/SkeletonLoader"; + +import type { RowData, BookingListingStatus } from "../types"; + +const descriptionByStatus: Record = { + upcoming: "upcoming_bookings", + recurring: "recurring_bookings", + past: "past_bookings", + cancelled: "cancelled_bookings", + unconfirmed: "unconfirmed_bookings", +}; + +type BookingsListViewProps = { + status: BookingListingStatus; + table: ReactTable; + isPending: boolean; + totalRowCount?: number; +}; + +export function BookingsList({ status, table, isPending, totalRowCount }: BookingsListViewProps) { + const { t } = useLocale(); + + return ( + + + + } + ToolbarRight={ + <> + + + + + } + LoaderView={} + EmptyView={ +
+ +
+ } + /> + ); +} diff --git a/apps/web/modules/bookings/types.ts b/apps/web/modules/bookings/types.ts new file mode 100644 index 0000000000..68bcaa935a --- /dev/null +++ b/apps/web/modules/bookings/types.ts @@ -0,0 +1,25 @@ +import type { RouterOutputs } from "@calcom/trpc/react"; + +import type { validStatuses } from "~/bookings/lib/validStatuses"; + +export type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; + +export type RecurringInfo = { + recurringEventId: string | null; + count: number; + firstDate: Date | null; + bookings: { [key: string]: Date[] }; +}; + +export type RowData = + | { + type: "data"; + booking: BookingOutput; + isToday: boolean; + recurringInfo?: RecurringInfo; + } + | { + type: "today" | "next"; + }; + +export type BookingListingStatus = (typeof validStatuses)[number]; diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-view.tsx similarity index 82% rename from apps/web/modules/bookings/views/bookings-listing-view.tsx rename to apps/web/modules/bookings/views/bookings-view.tsx index c4ccbab26e..00931e1a27 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-view.tsx @@ -2,57 +2,38 @@ import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table"; import { useSearchParams, usePathname } from "next/navigation"; -import { useMemo, useRef } from "react"; +import { createParser, useQueryState } from "nuqs"; +import { useMemo } from "react"; import dayjs from "@calcom/dayjs"; import { - useDataTable, DataTableProvider, - DataTableWrapper, - DataTableFilters, - DataTableSegment, + type SystemFilterSegment, + useDataTable, ColumnFilterType, useFilterValue, ZMultiSelectFilterValue, ZDateRangeFilterValue, ZTextFilterValue, - type SystemFilterSegment, } from "@calcom/features/data-table"; import { useSegments } from "@calcom/features/data-table/hooks/useSegments"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { Alert } from "@calcom/ui/components/alert"; -import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation"; import { HorizontalTabs } from "@calcom/ui/components/navigation"; import type { VerticalTabItemProps } from "@calcom/ui/components/navigation"; import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton"; import BookingListItem from "@components/booking/BookingListItem"; -import SkeletonLoader from "@components/booking/SkeletonLoader"; import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues"; import type { validStatuses } from "~/bookings/lib/validStatuses"; -type BookingListingStatus = (typeof validStatuses)[number]; -type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; - -type RecurringInfo = { - recurringEventId: string | null; - count: number; - firstDate: Date | null; - bookings: { [key: string]: Date[] }; -}; - -const descriptionByStatus: Record = { - upcoming: "upcoming_bookings", - recurring: "recurring_bookings", - past: "past_bookings", - cancelled: "cancelled_bookings", - unconfirmed: "unconfirmed_bookings", -}; +import { BookingsCalendar } from "../components/BookingsCalendar"; +import { BookingsList } from "../components/BookingsList"; +import type { RowData, BookingOutput } from "../types"; type BookingsProps = { status: (typeof validStatuses)[number]; @@ -103,21 +84,18 @@ export default function Bookings(props: BookingsProps) { ); } -type RowData = - | { - type: "data"; - booking: BookingOutput; - isToday: boolean; - recurringInfo?: RecurringInfo; - } - | { - type: "today" | "next"; - }; +const viewParser = createParser({ + parse: (value: string) => { + if (value === "calendar") return "calendar"; + return "list"; + }, + serialize: (value: "list" | "calendar") => value, +}); function BookingsContent({ status, permissions }: BookingsProps) { + const [view] = useQueryState("view", viewParser.withDefault("list")); const { t } = useLocale(); const user = useMeQuery().data; - const tableContainerRef = useRef(null); const searchParams = useSearchParams(); // Generate dynamic tabs that preserve query parameters @@ -411,6 +389,9 @@ function BookingsContent({ status, permissions }: BookingsProps) { getFacetedUniqueValues, }); + const isPending = query.isPending; + const totalRowCount = query.data?.totalCount; + return (
@@ -431,43 +412,16 @@ function BookingsContent({ status, permissions }: BookingsProps) { {!!bookingsToday.length && status === "upcoming" && ( )} - - - - } - ToolbarRight={ - <> - - - - - } - LoaderView={} - EmptyView={ -
- -
- } - /> + {view === "list" ? ( + + ) : ( + + )} )}
diff --git a/packages/features/data-table/GUIDE.md b/packages/features/data-table/GUIDE.md index fb4be41df3..c1d28d8324 100644 --- a/packages/features/data-table/GUIDE.md +++ b/packages/features/data-table/GUIDE.md @@ -976,7 +976,7 @@ From `packages/features/users/components/UserTable/UserListTable.tsx`: ### Example 2: Bookings List -From `apps/web/modules/bookings/views/bookings-listing-view.tsx`: +From `apps/web/modules/bookings/components/BookingsList.tsx`: ```tsx