Skip to content

Commit fddee71

Browse files
eunjae-leedevin-ai-integration[bot]cubic-dev-ai[bot]
authored
refactor: extract bookings list and calendar views (calcom#24486)
* 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 <hey@eunjae.dev> * 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 <hey@eunjae.dev> * 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 <hey@eunjae.dev> * 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 <hey@eunjae.dev> * 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>
1 parent 6f15a5a commit fddee71

7 files changed

Lines changed: 159 additions & 77 deletions

File tree

.agents/knowledge-base.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/
111111

112112
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
113113
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
114-
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
114+
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
115115
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
116116

117117
## When working on workflow triggers or similar enum-based features in the Cal.com codebase

apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
1212
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
1313

1414
import { validStatuses } from "~/bookings/lib/validStatuses";
15-
import BookingsList from "~/bookings/views/bookings-listing-view";
15+
import BookingsList from "~/bookings/views/bookings-view";
1616

1717
const querySchema = z.object({
1818
status: z.enum(validStatuses),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
3+
import type { Table as ReactTable } from "@tanstack/react-table";
4+
5+
import { DataTableFilters, DataTableSegment } from "@calcom/features/data-table";
6+
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
7+
8+
import type { RowData, BookingListingStatus } from "../types";
9+
10+
type BookingsCalendarViewProps = {
11+
status: BookingListingStatus;
12+
table: ReactTable<RowData>;
13+
};
14+
15+
export function BookingsCalendar({ table }: BookingsCalendarViewProps) {
16+
return (
17+
<>
18+
<div className="mb-4 flex items-center justify-between">
19+
<div className="flex items-center gap-2">
20+
<DataTableFilters.FilterBar table={table} />
21+
</div>
22+
23+
<div className="flex items-center gap-2">
24+
<DataTableFilters.ClearFiltersButton />
25+
<DataTableSegment.SaveButton />
26+
<DataTableSegment.Select />
27+
</div>
28+
</div>
29+
<div className="flex items-center justify-center pt-2 xl:pt-0">
30+
<EmptyScreen Icon="calendar" headline="Calendar view" description="Calendar view is coming soon." />
31+
</div>
32+
</>
33+
);
34+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client";
2+
3+
import type { Table as ReactTable } from "@tanstack/react-table";
4+
5+
import { DataTableWrapper, DataTableFilters, DataTableSegment } from "@calcom/features/data-table";
6+
import { useLocale } from "@calcom/lib/hooks/useLocale";
7+
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
8+
9+
import SkeletonLoader from "@components/booking/SkeletonLoader";
10+
11+
import type { RowData, BookingListingStatus } from "../types";
12+
13+
const descriptionByStatus: Record<BookingListingStatus, string> = {
14+
upcoming: "upcoming_bookings",
15+
recurring: "recurring_bookings",
16+
past: "past_bookings",
17+
cancelled: "cancelled_bookings",
18+
unconfirmed: "unconfirmed_bookings",
19+
};
20+
21+
type BookingsListViewProps = {
22+
status: BookingListingStatus;
23+
table: ReactTable<RowData>;
24+
isPending: boolean;
25+
totalRowCount?: number;
26+
};
27+
28+
export function BookingsList({ status, table, isPending, totalRowCount }: BookingsListViewProps) {
29+
const { t } = useLocale();
30+
31+
return (
32+
<DataTableWrapper
33+
className="mb-6"
34+
table={table}
35+
testId={`${status}-bookings`}
36+
bodyTestId="bookings"
37+
headerClassName="hidden"
38+
isPending={isPending}
39+
totalRowCount={totalRowCount}
40+
variant="compact"
41+
paginationMode="standard"
42+
ToolbarLeft={
43+
<>
44+
<DataTableFilters.FilterBar table={table} />
45+
</>
46+
}
47+
ToolbarRight={
48+
<>
49+
<DataTableFilters.ClearFiltersButton />
50+
<DataTableSegment.SaveButton />
51+
<DataTableSegment.Select />
52+
</>
53+
}
54+
LoaderView={<SkeletonLoader />}
55+
EmptyView={
56+
<div className="flex items-center justify-center pt-2 xl:pt-0">
57+
<EmptyScreen
58+
Icon="calendar"
59+
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
60+
description={t("no_status_bookings_yet_description", {
61+
status: t(status).toLowerCase(),
62+
description: t(descriptionByStatus[status]),
63+
})}
64+
/>
65+
</div>
66+
}
67+
/>
68+
);
69+
}

apps/web/modules/bookings/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { RouterOutputs } from "@calcom/trpc/react";
2+
3+
import type { validStatuses } from "~/bookings/lib/validStatuses";
4+
5+
export type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
6+
7+
export type RecurringInfo = {
8+
recurringEventId: string | null;
9+
count: number;
10+
firstDate: Date | null;
11+
bookings: { [key: string]: Date[] };
12+
};
13+
14+
export type RowData =
15+
| {
16+
type: "data";
17+
booking: BookingOutput;
18+
isToday: boolean;
19+
recurringInfo?: RecurringInfo;
20+
}
21+
| {
22+
type: "today" | "next";
23+
};
24+
25+
export type BookingListingStatus = (typeof validStatuses)[number];

apps/web/modules/bookings/views/bookings-listing-view.tsx renamed to apps/web/modules/bookings/views/bookings-view.tsx

Lines changed: 28 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,38 @@
22

33
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
44
import { useSearchParams, usePathname } from "next/navigation";
5-
import { useMemo, useRef } from "react";
5+
import { createParser, useQueryState } from "nuqs";
6+
import { useMemo } from "react";
67

78
import dayjs from "@calcom/dayjs";
89
import {
9-
useDataTable,
1010
DataTableProvider,
11-
DataTableWrapper,
12-
DataTableFilters,
13-
DataTableSegment,
11+
type SystemFilterSegment,
12+
useDataTable,
1413
ColumnFilterType,
1514
useFilterValue,
1615
ZMultiSelectFilterValue,
1716
ZDateRangeFilterValue,
1817
ZTextFilterValue,
19-
type SystemFilterSegment,
2018
} from "@calcom/features/data-table";
2119
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
2220
import { useLocale } from "@calcom/lib/hooks/useLocale";
23-
import type { RouterOutputs } from "@calcom/trpc/react";
2421
import { trpc } from "@calcom/trpc/react";
2522
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
2623
import { Alert } from "@calcom/ui/components/alert";
27-
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
2824
import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation";
2925
import { HorizontalTabs } from "@calcom/ui/components/navigation";
3026
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
3127
import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton";
3228

3329
import BookingListItem from "@components/booking/BookingListItem";
34-
import SkeletonLoader from "@components/booking/SkeletonLoader";
3530

3631
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
3732
import type { validStatuses } from "~/bookings/lib/validStatuses";
3833

39-
type BookingListingStatus = (typeof validStatuses)[number];
40-
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
41-
42-
type RecurringInfo = {
43-
recurringEventId: string | null;
44-
count: number;
45-
firstDate: Date | null;
46-
bookings: { [key: string]: Date[] };
47-
};
48-
49-
const descriptionByStatus: Record<BookingListingStatus, string> = {
50-
upcoming: "upcoming_bookings",
51-
recurring: "recurring_bookings",
52-
past: "past_bookings",
53-
cancelled: "cancelled_bookings",
54-
unconfirmed: "unconfirmed_bookings",
55-
};
34+
import { BookingsCalendar } from "../components/BookingsCalendar";
35+
import { BookingsList } from "../components/BookingsList";
36+
import type { RowData, BookingOutput } from "../types";
5637

5738
type BookingsProps = {
5839
status: (typeof validStatuses)[number];
@@ -103,21 +84,18 @@ export default function Bookings(props: BookingsProps) {
10384
);
10485
}
10586

106-
type RowData =
107-
| {
108-
type: "data";
109-
booking: BookingOutput;
110-
isToday: boolean;
111-
recurringInfo?: RecurringInfo;
112-
}
113-
| {
114-
type: "today" | "next";
115-
};
87+
const viewParser = createParser({
88+
parse: (value: string) => {
89+
if (value === "calendar") return "calendar";
90+
return "list";
91+
},
92+
serialize: (value: "list" | "calendar") => value,
93+
});
11694

11795
function BookingsContent({ status, permissions }: BookingsProps) {
96+
const [view] = useQueryState("view", viewParser.withDefault("list"));
11897
const { t } = useLocale();
11998
const user = useMeQuery().data;
120-
const tableContainerRef = useRef<HTMLDivElement>(null);
12199
const searchParams = useSearchParams();
122100

123101
// Generate dynamic tabs that preserve query parameters
@@ -411,6 +389,9 @@ function BookingsContent({ status, permissions }: BookingsProps) {
411389
getFacetedUniqueValues,
412390
});
413391

392+
const isPending = query.isPending;
393+
const totalRowCount = query.data?.totalCount;
394+
414395
return (
415396
<div className="flex flex-col">
416397
<div className="flex flex-row flex-wrap justify-between">
@@ -431,43 +412,16 @@ function BookingsContent({ status, permissions }: BookingsProps) {
431412
{!!bookingsToday.length && status === "upcoming" && (
432413
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
433414
)}
434-
<DataTableWrapper
435-
className="mb-6"
436-
tableContainerRef={tableContainerRef}
437-
table={table}
438-
testId={`${status}-bookings`}
439-
bodyTestId="bookings"
440-
headerClassName="hidden"
441-
isPending={query.isPending}
442-
totalRowCount={query.data?.totalCount}
443-
variant="compact"
444-
paginationMode="standard"
445-
ToolbarLeft={
446-
<>
447-
<DataTableFilters.FilterBar table={table} />
448-
</>
449-
}
450-
ToolbarRight={
451-
<>
452-
<DataTableFilters.ClearFiltersButton />
453-
<DataTableSegment.SaveButton />
454-
<DataTableSegment.Select />
455-
</>
456-
}
457-
LoaderView={<SkeletonLoader />}
458-
EmptyView={
459-
<div className="flex items-center justify-center pt-2 xl:pt-0">
460-
<EmptyScreen
461-
Icon="calendar"
462-
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
463-
description={t("no_status_bookings_yet_description", {
464-
status: t(status).toLowerCase(),
465-
description: t(descriptionByStatus[status]),
466-
})}
467-
/>
468-
</div>
469-
}
470-
/>
415+
{view === "list" ? (
416+
<BookingsList
417+
status={status}
418+
table={table}
419+
isPending={isPending}
420+
totalRowCount={totalRowCount}
421+
/>
422+
) : (
423+
<BookingsCalendar status={status} table={table} />
424+
)}
471425
</>
472426
)}
473427
</div>

packages/features/data-table/GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ From `packages/features/users/components/UserTable/UserListTable.tsx`:
976976

977977
### Example 2: Bookings List
978978

979-
From `apps/web/modules/bookings/views/bookings-listing-view.tsx`:
979+
From `apps/web/modules/bookings/components/BookingsList.tsx`:
980980

981981
```tsx
982982
<DataTableWrapper

0 commit comments

Comments
 (0)