Skip to content

Commit 8635319

Browse files
authored
feat: add booking actions into BookingDetailsSheet (calcom#24912)
* move booking actions into a subfolder * fix logic to render booking actions dropdown * skip dropdown portal when rendered from detail sheet * fix type error
1 parent 38f96d9 commit 8635319

10 files changed

Lines changed: 165 additions & 99 deletions

File tree

apps/web/components/booking/BookingListItem.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
4646
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
4747

4848
import { buildBookingLink } from "../../modules/bookings/lib/buildBookingLink";
49-
import { BookingActionsDropdown } from "./BookingActionsDropdown";
50-
import { useBookingActionsStoreContext, BookingActionsStoreProvider } from "./BookingActionsStoreProvider";
49+
import { BookingActionsDropdown } from "./actions/BookingActionsDropdown";
50+
import {
51+
useBookingActionsStoreContext,
52+
BookingActionsStoreProvider,
53+
} from "./actions/BookingActionsStoreProvider";
5154
import {
5255
getPendingActions,
5356
getCancelEventAction,
@@ -56,7 +59,7 @@ import {
5659
shouldShowIndividualReportButton,
5760
type BookingActionContext,
5861
getReportAction,
59-
} from "./bookingActions";
62+
} from "./actions/bookingActions";
6063
import type { BookingItemProps } from "./types";
6164

6265
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
@@ -496,7 +499,7 @@ function BookingListItem(booking: BookingItemProps) {
496499
</div>
497500
<div className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
498501
{shouldShowPendingActions(actionContext) && <TableActions actions={pendingActions} />}
499-
<BookingActionsDropdown booking={booking} />
502+
<BookingActionsDropdown booking={booking} context="booking-list-item" />
500503
{shouldShowRecurringCancelAction(actionContext) && <TableActions actions={[cancelEventAction]} />}
501504
{shouldShowIndividualReportButton(actionContext) && (
502505
<div className="flex items-center space-x-2">

apps/web/components/booking/BookingActionsDropdown.tsx renamed to apps/web/components/booking/actions/BookingActionsDropdown.tsx

Lines changed: 112 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
3131
import { RerouteDialog } from "@components/dialog/RerouteDialog";
3232
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
3333

34+
import type { BookingItemProps } from "../types";
3435
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
3536
import {
3637
getCancelEventAction,
@@ -40,13 +41,14 @@ import {
4041
shouldShowEditActions,
4142
type BookingActionContext,
4243
} from "./bookingActions";
43-
import type { BookingItemProps } from "./types";
4444

4545
interface BookingActionsDropdownProps {
4646
booking: BookingItemProps;
47+
context: "booking-list-item" | "booking-details-sheet";
48+
size?: "xs" | "sm" | "base" | "lg";
4749
}
4850

49-
export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps) {
51+
export function BookingActionsDropdown({ booking, context, size = "base" }: BookingActionsDropdownProps) {
5052
const { t } = useLocale();
5153
const utils = trpc.useUtils();
5254

@@ -236,9 +238,11 @@ export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps)
236238

237239
const cancelEventAction = getCancelEventAction(actionContext);
238240

241+
const shouldShowEdit = shouldShowEditActions(actionContext);
239242
const baseEditEventActions = getEditEventActions(actionContext);
240243
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
241244
...action,
245+
disabled: !shouldShowEdit || action.disabled, // Disable all edit actions if shouldn't show edit actions
242246
onClick:
243247
action.id === "reschedule_request"
244248
? () => setIsOpenRescheduleDialog(true)
@@ -494,99 +498,119 @@ export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps)
494498
</>
495499
);
496500

497-
// Don't render dropdown if edit actions shouldn't be shown
498-
if (!shouldShowEditActions(actionContext)) {
501+
// Check if there are any available actions across all action groups
502+
const hasAnyAvailableActions = () => {
503+
// Check if any edit action is available
504+
const hasAvailableEditAction = editEventActions.some((action) => !action.disabled);
505+
506+
// Check if any after event action is available
507+
const hasAvailableAfterAction = afterEventActions.some((action) => !action.disabled);
508+
509+
// For booking-list-item context, only check edit and after event actions
510+
if (context === "booking-list-item") {
511+
return hasAvailableEditAction || hasAvailableAfterAction;
512+
}
513+
514+
// For booking-details-sheet context, also check report and cancel actions
515+
const isReportAvailable = !reportActionWithHandler.disabled;
516+
const isCancelAvailable = !cancelEventAction.disabled;
517+
518+
return hasAvailableEditAction || hasAvailableAfterAction || isReportAvailable || isCancelAvailable;
519+
};
520+
521+
// Don't render dropdown if no actions are available
522+
if (!hasAnyAvailableActions()) {
499523
return dialogs;
500524
}
501525

526+
const menuContent = (
527+
<DropdownMenuContent>
528+
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("edit_event")}</DropdownMenuLabel>
529+
{editEventActions.map((action) => (
530+
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
531+
<DropdownItem
532+
type="button"
533+
color={action.color}
534+
StartIcon={action.icon}
535+
href={action.href}
536+
disabled={action.disabled}
537+
onClick={action.onClick}
538+
data-bookingid={action.bookingId}
539+
data-testid={action.id}
540+
className={action.disabled ? "text-muted" : undefined}>
541+
{action.label}
542+
</DropdownItem>
543+
</DropdownMenuItem>
544+
))}
545+
<DropdownMenuSeparator />
546+
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("after_event")}</DropdownMenuLabel>
547+
{afterEventActions.map((action) => (
548+
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
549+
<DropdownItem
550+
type="button"
551+
color={action.color}
552+
StartIcon={action.icon}
553+
href={action.href}
554+
onClick={action.onClick}
555+
disabled={action.disabled}
556+
data-bookingid={action.bookingId}
557+
data-testid={action.id}
558+
className={action.disabled ? "text-muted" : undefined}>
559+
{action.label}
560+
</DropdownItem>
561+
</DropdownMenuItem>
562+
))}
563+
<>
564+
<DropdownMenuSeparator />
565+
<DropdownMenuItem
566+
className="rounded-lg"
567+
key={reportActionWithHandler.id}
568+
disabled={reportActionWithHandler.disabled}>
569+
<DropdownItem
570+
type="button"
571+
color={reportActionWithHandler.color}
572+
StartIcon={reportActionWithHandler.icon}
573+
onClick={reportActionWithHandler.onClick}
574+
disabled={reportActionWithHandler.disabled}
575+
data-testid={reportActionWithHandler.id}
576+
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
577+
{reportActionWithHandler.label}
578+
</DropdownItem>
579+
</DropdownMenuItem>
580+
</>
581+
<DropdownMenuSeparator />
582+
<DropdownMenuItem
583+
className="rounded-lg"
584+
key={cancelEventAction.id}
585+
disabled={cancelEventAction.disabled}>
586+
<DropdownItem
587+
type="button"
588+
color={cancelEventAction.color}
589+
StartIcon={cancelEventAction.icon}
590+
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
591+
onClick={cancelEventAction.onClick}
592+
disabled={cancelEventAction.disabled}
593+
data-bookingid={cancelEventAction.bookingId}
594+
data-testid={cancelEventAction.id}
595+
className={cancelEventAction.disabled ? "text-muted" : undefined}>
596+
{cancelEventAction.label}
597+
</DropdownItem>
598+
</DropdownMenuItem>
599+
</DropdownMenuContent>
600+
);
601+
502602
return (
503603
<>
504604
{dialogs}
505-
<Dropdown>
506-
<DropdownMenuTrigger asChild>
507-
<Button
508-
type="button"
509-
color="secondary"
510-
variant="icon"
511-
StartIcon="ellipsis"
512-
data-testid="booking-actions-dropdown"
513-
/>
605+
<Dropdown modal={context === "booking-details-sheet" ? false : undefined}>
606+
<DropdownMenuTrigger asChild data-testid="booking-actions-dropdown">
607+
<Button type="button" color="secondary" variant="icon" size={size} StartIcon="ellipsis" />
514608
</DropdownMenuTrigger>
515-
<DropdownMenuPortal>
516-
<DropdownMenuContent>
517-
<DropdownMenuLabel className="p-2">{t("edit_event")}</DropdownMenuLabel>
518-
{editEventActions.map((action) => (
519-
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
520-
<DropdownItem
521-
type="button"
522-
color={action.color}
523-
StartIcon={action.icon}
524-
href={action.href}
525-
disabled={action.disabled}
526-
onClick={action.onClick}
527-
data-bookingid={action.bookingId}
528-
data-testid={action.id}
529-
className={action.disabled ? "text-muted" : undefined}>
530-
{action.label}
531-
</DropdownItem>
532-
</DropdownMenuItem>
533-
))}
534-
<DropdownMenuSeparator className="!my-1 bg-subtle" />
535-
<DropdownMenuLabel className="p-2">{t("after_event")}</DropdownMenuLabel>
536-
{afterEventActions.map((action) => (
537-
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
538-
<DropdownItem
539-
type="button"
540-
color={action.color}
541-
StartIcon={action.icon}
542-
href={action.href}
543-
onClick={action.onClick}
544-
disabled={action.disabled}
545-
data-bookingid={action.bookingId}
546-
data-testid={action.id}
547-
className={action.disabled ? "text-muted" : undefined}>
548-
{action.label}
549-
</DropdownItem>
550-
</DropdownMenuItem>
551-
))}
552-
<>
553-
<DropdownMenuSeparator className="!my-1 bg-subtle" />
554-
<DropdownMenuItem
555-
className="rounded-lg"
556-
key={reportActionWithHandler.id}
557-
disabled={reportActionWithHandler.disabled}>
558-
<DropdownItem
559-
type="button"
560-
color={reportActionWithHandler.color}
561-
StartIcon={reportActionWithHandler.icon}
562-
onClick={reportActionWithHandler.onClick}
563-
disabled={reportActionWithHandler.disabled}
564-
data-testid={reportActionWithHandler.id}
565-
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
566-
{reportActionWithHandler.label}
567-
</DropdownItem>
568-
</DropdownMenuItem>
569-
</>
570-
<DropdownMenuSeparator className="!my-1 bg-subtle" />
571-
<DropdownMenuItem
572-
className="rounded-lg"
573-
key={cancelEventAction.id}
574-
disabled={cancelEventAction.disabled}>
575-
<DropdownItem
576-
type="button"
577-
color={cancelEventAction.color}
578-
StartIcon={cancelEventAction.icon}
579-
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
580-
onClick={cancelEventAction.onClick}
581-
disabled={cancelEventAction.disabled}
582-
data-bookingid={cancelEventAction.bookingId}
583-
data-testid={cancelEventAction.id}
584-
className={cancelEventAction.disabled ? "text-muted" : undefined}>
585-
{cancelEventAction.label}
586-
</DropdownItem>
587-
</DropdownMenuItem>
588-
</DropdownMenuContent>
589-
</DropdownMenuPortal>
609+
{context === "booking-details-sheet" ? (
610+
menuContent
611+
) : (
612+
<DropdownMenuPortal>{menuContent}</DropdownMenuPortal>
613+
)}
590614
</Dropdown>
591615
</>
592616
);

apps/web/components/booking/BookingActionsStoreProvider.tsx renamed to apps/web/components/booking/actions/BookingActionsStoreProvider.tsx

File renamed without changes.

apps/web/components/booking/bookingActions.test.ts renamed to apps/web/components/booking/actions/bookingActions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function createMockContext(overrides: Partial<BookingActionContext> = {}): Booki
5454
locale: "en",
5555
bookingId: 1,
5656
noShow: false,
57-
} as any,
57+
},
5858
],
5959
user: {
6060
id: 1,

apps/web/components/booking/bookingActions.ts renamed to apps/web/components/booking/actions/bookingActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
22
import type { ActionType } from "@calcom/ui/components/table";
33

4-
import type { BookingItemProps } from "./types";
4+
import type { BookingItemProps } from "../types";
55

66
export interface BookingActionContext {
77
booking: BookingItemProps;
File renamed without changes.

apps/web/components/booking/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
22

3-
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
3+
export type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
44

55
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
66

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import {
2626
SheetTitle,
2727
} from "@calcom/ui/components/sheet";
2828

29+
import { BookingActionsDropdown } from "../../../components/booking/actions/BookingActionsDropdown";
30+
import { BookingActionsStoreProvider } from "../../../components/booking/actions/BookingActionsStoreProvider";
31+
import type { BookingListingStatus } from "../../../components/booking/types";
2932
import { buildBookingLink } from "../lib/buildBookingLink";
3033
import type { BookingOutput } from "../types";
3134

@@ -37,26 +40,43 @@ interface BookingDetailsSheetProps {
3740
onClose: () => void;
3841
userTimeZone?: string;
3942
userTimeFormat?: number;
43+
userId?: number;
44+
userEmail?: string;
4045
onPrevious?: () => void;
4146
hasPrevious?: boolean;
4247
onNext?: () => void;
4348
hasNext?: boolean;
4449
}
4550

46-
export function BookingDetailsSheet({
51+
interface BookingDetailsSheetInnerProps extends Omit<BookingDetailsSheetProps, "booking"> {
52+
booking: BookingOutput;
53+
}
54+
55+
export function BookingDetailsSheet(props: BookingDetailsSheetProps) {
56+
if (!props.booking) return null;
57+
58+
return (
59+
<BookingActionsStoreProvider>
60+
<BookingDetailsSheetInner {...props} booking={props.booking} />
61+
</BookingActionsStoreProvider>
62+
);
63+
}
64+
65+
function BookingDetailsSheetInner({
4766
booking,
4867
isOpen,
4968
onClose,
5069
userTimeZone,
70+
userTimeFormat,
71+
userId,
72+
userEmail,
5173
onPrevious,
5274
hasPrevious = false,
5375
onNext,
5476
hasNext = false,
55-
}: BookingDetailsSheetProps) {
77+
}: BookingDetailsSheetInnerProps) {
5678
const { t } = useLocale();
5779

58-
if (!booking) return null;
59-
6080
const startTime = dayjs(booking.startTime).tz(userTimeZone);
6181
const endTime = dayjs(booking.endTime).tz(userTimeZone);
6282

@@ -210,6 +230,22 @@ export function BookingDetailsSheet({
210230
<Button color="secondary" size="sm" EndIcon="external-link" href={bookingLink} target="_blank">
211231
{t("view")}
212232
</Button>
233+
<BookingActionsDropdown
234+
context="booking-details-sheet"
235+
size="sm"
236+
booking={{
237+
...booking,
238+
listingStatus: booking.status.toLowerCase() as BookingListingStatus,
239+
recurringInfo: undefined,
240+
loggedInUser: {
241+
userId,
242+
userTimeZone,
243+
userTimeFormat: userTimeFormat ?? null,
244+
userEmail,
245+
},
246+
isToday: false,
247+
}}
248+
/>
213249
</div>
214250
</SheetFooter>
215251
</SheetContent>

apps/web/modules/bookings/views/bookings-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ function BookingsContent({ status, permissions, isCalendarViewEnabled }: Booking
473473
onClose={() => setSelectedBookingId(null)}
474474
userTimeZone={user?.timeZone}
475475
userTimeFormat={user?.timeFormat === null ? undefined : user?.timeFormat}
476+
userId={user?.id}
477+
userEmail={user?.email}
476478
onPrevious={bookingNavigation.onPrevious}
477479
hasPrevious={bookingNavigation.hasPrevious}
478480
onNext={bookingNavigation.onNext}

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3968,5 +3968,6 @@
39683968
"team_invite_subtitle": "Invite team members to collaborate and schedule together",
39693969
"team_onboarding_details_subtitle": "Add your team's name and create a unique URL for your team",
39703970
"view_booking_details": "View Booking Details",
3971+
"view": "View",
39713972
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
39723973
}

0 commit comments

Comments
 (0)