Skip to content

Commit 2bc1731

Browse files
hariombalharadevin-ai-integration[bot]Udit-takkar
authored
fix: deep link reschedule audit log to booking drawer history tab (calcom#27709)
* fix: deep link reschedule audit log to booking drawer history tab Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: make booking drawer tab-agnostic for cross-tab deep links Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use status-agnostic /bookings URL for audit log deep links - Update audit service URLs from /bookings/upcoming?uid=... to /bookings?uid=... - Add /bookings/page.tsx redirect that routes to /bookings/upcoming preserving query params - Update tests to expect new URL format Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * refactor: use client-side replaceState instead of server redirect for booking deep links Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use router.replace instead of replaceState to update tab and booking list Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * improvements * refactor: extract deep link logic from BookingListContainer into usePreSelectedBooking hook Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use proper BookingOutput status type in test helper Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: decouple getTabForBooking from BookingOutput type for simpler testing Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: accept Date | string for endTime in BookingForTabResolution interface Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: eliminate initialBookingUid prop drilling and revert formatting-only changes Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use AuditDeepLink wrapper to preserve target=_blank through ServerTrans cloneElement Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Improve code organization * refactor: rename usePreSelectedBooking to useSwitchToCorrectStatusTab Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: push preSelectedBooking into store so drawer opens on direct navigation Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * fixes * fixes * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
1 parent 3a7122d commit 2bc1731

9 files changed

Lines changed: 454 additions & 95 deletions

File tree

apps/web/modules/booking-audit/components/BookingHistory.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ function ActionTitle({ actionDisplayTitle }: { actionDisplayTitle: TranslationWi
139139
values={actionDisplayTitle.params}
140140
components={actionDisplayTitle.components.map((comp) =>
141141
comp.type === "link" ? (
142-
<Link key={comp.href} href={comp.href} className="text-emphasis underline hover:no-underline" />
142+
<Link
143+
key={comp.href}
144+
href={comp.href}
145+
target="_blank"
146+
rel="noopener noreferrer"
147+
className="text-emphasis underline hover:no-underline"
148+
/>
143149
) : (
144150
<span key={comp.href} />
145151
)
@@ -304,15 +310,15 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
304310
{/* Render displayFields if available, otherwise show type */}
305311
{log.displayFields && log.displayFields.length > 0
306312
? log.displayFields.map((field, idx) => (
307-
<div
308-
key={idx}
309-
className="flex items-start gap-2 py-2 border-b px-3 border-subtle">
310-
<span className="font-medium text-emphasis w-[140px]">{t(field.labelKey)}</span>
311-
<span className="font-medium">
312-
<DisplayFieldValue field={field} />
313-
</span>
314-
</div>
315-
))
313+
<div
314+
key={idx}
315+
className="flex items-start gap-2 py-2 border-b px-3 border-subtle">
316+
<span className="font-medium text-emphasis w-[140px]">{t(field.labelKey)}</span>
317+
<span className="font-medium">
318+
<DisplayFieldValue field={field} />
319+
</span>
320+
</div>
321+
))
316322
: null}
317323
<div className="flex items-start gap-2 py-2 border-b px-3 border-subtle">
318324
<span className="font-medium text-emphasis w-[140px]">{t("actor")}</span>

apps/web/modules/bookings/components/BookingListContainer.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import { ToggleGroup } from "@calcom/ui/components/form";
1313
import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton";
1414
import { getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
1515
import { useRouter } from "next/navigation";
16-
import React, { useCallback, useEffect, useMemo, useState } from "react";
16+
import { useCallback, useEffect, useMemo, useState } from "react";
1717
import { useBookingFilters } from "~/bookings/hooks/useBookingFilters";
1818
import { useBookingListColumns } from "~/bookings/hooks/useBookingListColumns";
1919
import { useBookingListData } from "~/bookings/hooks/useBookingListData";
2020
import { useBookingStatusTab } from "~/bookings/hooks/useBookingStatusTab";
2121
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
2222
import { useListAutoSelector } from "~/bookings/hooks/useListAutoSelector";
23+
import { useSwitchToCorrectStatusTab } from "~/bookings/hooks/useSwitchToCorrectStatusTab";
2324
import { DataTableFilters, DataTableSegment } from "~/data-table/components";
2425
import {
2526
BookingDetailsSheetStoreProvider,
@@ -231,17 +232,21 @@ function BookingListInner({
231232
}
232233

233234
export function BookingListContainer(props: BookingListContainerProps) {
234-
const { limit, offset, setPageIndex, isValidatorPending } = useDataTable();
235+
const { limit, offset, isValidatorPending } = useDataTable();
235236
const { eventTypeIds, teamIds, userIds, dateRange, attendeeName, attendeeEmail, bookingUid } =
236237
useBookingFilters();
237238

239+
const { resolvedTabStatus, isResolvingTabStatus, preSelectedBooking } = useSwitchToCorrectStatusTab({
240+
defaultStatus: props.status,
241+
});
242+
238243
// Build query input once - shared between query and prefetching
239244
const queryInput = useMemo(
240245
() => ({
241246
limit,
242247
offset,
243248
filters: {
244-
statuses: [props.status],
249+
statuses: [resolvedTabStatus],
245250
eventTypeIds,
246251
teamIds,
247252
userIds,
@@ -257,7 +262,7 @@ export function BookingListContainer(props: BookingListContainerProps) {
257262
[
258263
limit,
259264
offset,
260-
props.status,
265+
resolvedTabStatus,
261266
eventTypeIds,
262267
teamIds,
263268
userIds,
@@ -271,10 +276,19 @@ export function BookingListContainer(props: BookingListContainerProps) {
271276
const query = trpc.viewer.bookings.get.useQuery(queryInput, {
272277
staleTime: 5 * 60 * 1000, // 5 minutes - data is considered fresh
273278
gcTime: 30 * 60 * 1000, // 30 minutes - cache retention time
274-
enabled: !isValidatorPending, // Wait for validator to be ready before fetching
279+
// We wait for tab status to be resolved before fetching, so that we can fetch the correct bookings as per resolved tab status
280+
enabled: !isValidatorPending && !isResolvingTabStatus, // Wait for validator to be ready before fetching
275281
});
276282

277-
const bookings = useMemo(() => query.data?.bookings ?? [], [query.data?.bookings]);
283+
const bookings = useMemo(() => {
284+
const queryBookings = query.data?.bookings ?? [];
285+
if (!preSelectedBooking) return queryBookings;
286+
if (queryBookings.some((b) => b.uid === preSelectedBooking.uid)) return queryBookings;
287+
// It ensures that the drawer opens for a booking that isn't even in the bookings list
288+
// Note that, bookings list doesn't use this so, it won't be visible in the list view but drawer will open for it
289+
// We don't want to show this booking in the list view, as it might not match the filters/pagination applied
290+
return [...queryBookings, preSelectedBooking];
291+
}, [query.data?.bookings, preSelectedBooking]);
278292

279293
// Always call the hook and provide navigation capabilities
280294
// The BookingDetailsSheet is only rendered when bookingsV3Enabled is true (see line 212)
@@ -290,6 +304,7 @@ export function BookingListContainer(props: BookingListContainerProps) {
290304
<BookingDetailsSheetStoreProvider bookings={bookings}>
291305
<BookingListInner
292306
{...props}
307+
status={resolvedTabStatus}
293308
data={query.data}
294309
isPending={query.isPending}
295310
hasError={!!query.error}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import { getTabForBooking } from "./useSwitchToCorrectStatusTab";
4+
5+
function makeBooking(overrides: {
6+
status: string;
7+
endTime: Date;
8+
recurringEventId?: string | null;
9+
}){
10+
return {
11+
status: overrides.status,
12+
endTime: overrides.endTime,
13+
recurringEventId: overrides.recurringEventId ?? null,
14+
};
15+
}
16+
17+
const future = new Date(Date.now() + 24 * 60 * 60 * 1000);
18+
const past = new Date(Date.now() - 24 * 60 * 60 * 1000);
19+
20+
describe("getTabForBooking", () => {
21+
it("returns 'cancelled' for CANCELLED bookings", () => {
22+
expect(getTabForBooking(makeBooking({ status: "CANCELLED", endTime: future }))).toBe("cancelled");
23+
});
24+
25+
it("returns 'cancelled' for REJECTED bookings", () => {
26+
expect(getTabForBooking(makeBooking({ status: "REJECTED", endTime: future }))).toBe("cancelled");
27+
});
28+
29+
it("returns 'cancelled' for CANCELLED bookings even if past", () => {
30+
expect(getTabForBooking(makeBooking({ status: "CANCELLED", endTime: past }))).toBe("cancelled");
31+
});
32+
33+
it("returns 'unconfirmed' for PENDING bookings in the future", () => {
34+
expect(getTabForBooking(makeBooking({ status: "PENDING", endTime: future }))).toBe("unconfirmed");
35+
});
36+
37+
it("returns 'past' for PENDING bookings that have ended", () => {
38+
expect(getTabForBooking(makeBooking({ status: "PENDING", endTime: past }))).toBe("past");
39+
});
40+
41+
it("returns 'past' for ACCEPTED bookings that have ended", () => {
42+
expect(getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: past }))).toBe("past");
43+
});
44+
45+
it("returns 'recurring' for future ACCEPTED bookings with recurringEventId", () => {
46+
expect(
47+
getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: future, recurringEventId: "rec-123" }))
48+
).toBe("recurring");
49+
});
50+
51+
it("returns 'upcoming' for future ACCEPTED bookings without recurringEventId", () => {
52+
expect(getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: future }))).toBe("upcoming");
53+
});
54+
55+
it("returns 'past' for recurring bookings that have ended", () => {
56+
expect(
57+
getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: past, recurringEventId: "rec-123" }))
58+
).toBe("past");
59+
});
60+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { trpc } from "@calcom/trpc/react";
2+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
3+
import { useEffect, useState, useTransition } from "react";
4+
import { validStatuses } from "../lib/validStatuses";
5+
import type { BookingListingStatus, BookingOutput } from "../types";
6+
7+
interface BookingForTabResolution {
8+
status: string;
9+
endTime: Date | string;
10+
recurringEventId: string | null;
11+
}
12+
13+
interface UseSwitchToCorrectTabResult {
14+
resolvedTabStatus: BookingListingStatus;
15+
isResolvingTabStatus: boolean;
16+
preSelectedBooking: BookingOutput | null;
17+
}
18+
19+
export function getTabForBooking(booking: BookingForTabResolution): BookingListingStatus {
20+
const isPast = new Date(booking.endTime) <= new Date();
21+
22+
if (booking.status === "CANCELLED" || booking.status === "REJECTED") {
23+
return "cancelled";
24+
}
25+
if (booking.status === "PENDING" && !isPast) {
26+
return "unconfirmed";
27+
}
28+
if (isPast) {
29+
return "past";
30+
}
31+
if (booking.recurringEventId) {
32+
return "recurring";
33+
}
34+
return "upcoming";
35+
}
36+
37+
/**
38+
* Ensures that the correct status tab is selected based on the pre-selected booking through query params
39+
*/
40+
export function useSwitchToCorrectStatusTab({
41+
defaultStatus,
42+
}: {
43+
defaultStatus: BookingListingStatus;
44+
}): UseSwitchToCorrectTabResult {
45+
const {
46+
preSelectedBookingUid,
47+
preSelectedBooking,
48+
preSelectedBookingFull,
49+
isPending: isFetchingPreSelectedBooking,
50+
} = usePreSelectedBooking();
51+
const pathname = usePathname();
52+
const router = useRouter();
53+
const [resolvedTabStatus, setResolvedTab] = useState<BookingListingStatus>(defaultStatus);
54+
const [isNavigatingToCorrectTab, startNavigationToCorrectTab] = useTransition();
55+
56+
useEffect(() => {
57+
if (!preSelectedBooking) return;
58+
const correctTab = getTabForBooking(preSelectedBooking);
59+
const currentTab = pathname?.match(/\/bookings\/(\w+)/)?.[1];
60+
const shouldNavigate = correctTab && currentTab && correctTab !== currentTab;
61+
if (!shouldNavigate) return;
62+
startNavigationToCorrectTab(() => {
63+
const newPath = pathname.replace(`/bookings/${currentTab}`, `/bookings/${correctTab}`);
64+
router.replace(`${newPath}${window.location.search}`);
65+
});
66+
setResolvedTab(correctTab);
67+
}, [preSelectedBooking, pathname, router]);
68+
69+
const isResolvingTabStatus = preSelectedBookingUid
70+
? isFetchingPreSelectedBooking || isNavigatingToCorrectTab
71+
: false;
72+
73+
return { resolvedTabStatus, isResolvingTabStatus, preSelectedBooking: preSelectedBookingFull };
74+
}
75+
76+
export function usePreSelectedBooking(): {
77+
preSelectedBookingUid: string | undefined;
78+
preSelectedBooking: BookingForTabResolution | null;
79+
preSelectedBookingFull: BookingOutput | null;
80+
isPending: boolean;
81+
}{
82+
const searchParams = useSearchParams();
83+
const preSelectedBookingUid = searchParams?.get("uid") ?? undefined;
84+
85+
const { data: preSelectedBookingData, isPending } = trpc.viewer.bookings.get.useQuery(
86+
{
87+
limit: 1,
88+
offset: 0,
89+
filters: {
90+
bookingUid: preSelectedBookingUid,
91+
statuses: [...validStatuses],
92+
},
93+
},
94+
{
95+
enabled: !!preSelectedBookingUid,
96+
staleTime: 5 * 60 * 1000,
97+
}
98+
);
99+
100+
const preSelectedBookingFull = preSelectedBookingData?.bookings?.[0] ?? null;
101+
const preSelectedBooking: BookingForTabResolution | null = preSelectedBookingFull;
102+
return { preSelectedBookingUid, preSelectedBooking, preSelectedBookingFull, isPending };
103+
}

packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { z } from "zod";
2-
32
import { NumberChangeSchema, StringChangeSchema } from "../common/changeSchemas";
43
import type { DataRequirements } from "../service/EnrichmentDataStore";
54
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
65
import type {
6+
BaseStoredAuditData,
7+
GetDisplayJsonParams,
8+
GetDisplayTitleParams,
79
IAuditActionService,
810
TranslationWithParams,
9-
GetDisplayTitleParams,
10-
GetDisplayJsonParams,
11-
BaseStoredAuditData,
1211
} from "./IAuditActionService";
1312

1413
/**
@@ -91,7 +90,7 @@ export class RescheduledAuditActionService implements IAuditActionService {
9190
newDate,
9291
},
9392
components: rescheduledToUid
94-
? [{ type: "link", href: `/booking/${rescheduledToUid}/logs` }]
93+
? [{ type: "link", href: `/bookings?uid=${rescheduledToUid}&activeSegment=history` }]
9594
: undefined,
9695
};
9796
}
@@ -122,7 +121,7 @@ export class RescheduledAuditActionService implements IAuditActionService {
122121
oldDate,
123122
newDate,
124123
},
125-
components: [{ type: "link", href: `/booking/${fromRescheduleUid}/logs` }],
124+
components: [{ type: "link", href: `/bookings?uid=${fromRescheduleUid}&activeSegment=history` }],
126125
};
127126
}
128127

packages/features/booking-audit/lib/actions/SeatRescheduledAuditActionService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ export class SeatRescheduledAuditActionService implements IAuditActionService {
8989
oldDate,
9090
newDate,
9191
},
92-
components: rescheduledToBookingUid
93-
? [{ type: "link", href: `/booking/${rescheduledToBookingUid}/logs` }]
94-
: undefined,
92+
components: rescheduledToBookingUid
93+
? [{ type: "link", href: `/bookings?uid=${rescheduledToBookingUid}&activeSegment=history` }]
94+
: undefined,
9595
};
9696
}
9797

0 commit comments

Comments
 (0)