}
{shouldShowIndividualReportButton(actionContext) && (
diff --git a/apps/web/components/booking/actions/BookingActionsDropdown.tsx b/apps/web/components/booking/actions/BookingActionsDropdown.tsx
index 13c04b8915..66cc5b5ea7 100644
--- a/apps/web/components/booking/actions/BookingActionsDropdown.tsx
+++ b/apps/web/components/booking/actions/BookingActionsDropdown.tsx
@@ -15,9 +15,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
+ DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuTrigger,
- DropdownMenuPortal,
} from "@calcom/ui/components/dropdown";
import { TextAreaField } from "@calcom/ui/components/form";
import type { ActionType } from "@calcom/ui/components/table";
@@ -31,6 +31,7 @@ import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
+import { buildBookingLink } from "../../../modules/bookings/lib/buildBookingLink";
import type { BookingItemProps } from "../types";
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
import {
@@ -39,16 +40,29 @@ import {
getAfterEventActions,
getReportAction,
shouldShowEditActions,
+ shouldShowPendingActions,
+ getPendingActions,
type BookingActionContext,
} from "./bookingActions";
interface BookingActionsDropdownProps {
booking: BookingItemProps;
- context: "booking-list-item" | "booking-details-sheet";
size?: "xs" | "sm" | "base" | "lg";
+ className?: string;
+ /**
+ * Whether to use a portal for the dropdown menu.
+ * Set to false when rendering inside a Sheet/Dialog to keep the dropdown within the modal's stacking context.
+ * @default true
+ */
+ usePortal?: boolean;
}
-export function BookingActionsDropdown({ booking, context, size = "base" }: BookingActionsDropdownProps) {
+export function BookingActionsDropdown({
+ booking,
+ size = "base",
+ className,
+ usePortal = true,
+}: BookingActionsDropdownProps) {
const { t } = useLocale();
const utils = trpc.useUtils();
@@ -164,6 +178,13 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
const isBookingFromRoutingForm = !!booking.routedFromRoutingFormReponse && !!booking.eventType?.team;
+ // Build booking confirmation link
+ const bookingLink = buildBookingLink({
+ bookingUid: booking.uid,
+ allRemainingBookings: isRecurring,
+ email: booking.attendees?.[0]?.email,
+ });
+
const userEmail = booking.loggedInUser.userEmail;
const userSeat = booking.seatsReferences.find((seat) => !!userEmail && seat.attendee?.email === userEmail);
const isAttendee = !!userSeat;
@@ -211,6 +232,10 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
}
};
+ // Calculate showPendingPayment based on payment logic
+ const hasPayment = booking.payment.length > 0;
+ const showPendingPayment = hasPayment;
+
const actionContext: BookingActionContext = {
booking,
isUpcoming,
@@ -228,7 +253,7 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
isDisabledCancelling,
isDisabledRescheduling,
isCalVideoLocation,
- showPendingPayment: false, // This will be calculated below
+ showPendingPayment,
isAttendee,
cardCharged,
attendeeList,
@@ -238,6 +263,20 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
const cancelEventAction = getCancelEventAction(actionContext);
+ // Get pending actions (accept/reject)
+ const shouldShowPending = shouldShowPendingActions(actionContext);
+ const basePendingActions = shouldShowPending ? getPendingActions(actionContext) : [];
+ const pendingActions: ActionType[] = basePendingActions.map((action) => ({
+ ...action,
+ disabled: mutation.isPending,
+ onClick:
+ action.id === "confirm"
+ ? () => bookingConfirm(true)
+ : action.id === "reject"
+ ? () => setRejectionDialogIsOpen(true)
+ : undefined,
+ })) as ActionType[];
+
const shouldShowEdit = shouldShowEditActions(actionContext);
const baseEditEventActions = getEditEventActions(actionContext);
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
@@ -500,22 +539,26 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
// Check if there are any available actions across all action groups
const hasAnyAvailableActions = () => {
+ // Check if any pending action is available
+ const hasAvailablePendingAction = pendingActions.some((action) => !action.disabled);
+
// Check if any edit action is available
const hasAvailableEditAction = editEventActions.some((action) => !action.disabled);
// Check if any after event action is available
const hasAvailableAfterAction = afterEventActions.some((action) => !action.disabled);
- // For booking-list-item context, only check edit and after event actions
- if (context === "booking-list-item") {
- return hasAvailableEditAction || hasAvailableAfterAction;
- }
-
- // For booking-details-sheet context, also check report and cancel actions
+ // Check report and cancel actions
const isReportAvailable = !reportActionWithHandler.disabled;
const isCancelAvailable = !cancelEventAction.disabled;
- return hasAvailableEditAction || hasAvailableAfterAction || isReportAvailable || isCancelAvailable;
+ return (
+ hasAvailablePendingAction ||
+ hasAvailableEditAction ||
+ hasAvailableAfterAction ||
+ isReportAvailable ||
+ isCancelAvailable
+ );
};
// Don't render dropdown if no actions are available
@@ -523,94 +566,150 @@ export function BookingActionsDropdown({ booking, context, size = "base" }: Book
return dialogs;
}
- const menuContent = (
-
- {t("edit_event")}
- {editEventActions.map((action) => (
-
-
- {action.label}
-
-
- ))}
-
- {t("after_event")}
- {afterEventActions.map((action) => (
-
-
- {action.label}
-
-
- ))}
- <>
-
-
-
- {reportActionWithHandler.label}
-
-
- >
-
-
-
- {cancelEventAction.label}
-
-
-
- );
+ // Conditional portal wrapper to avoid portal when inside Sheet/Dialog
+ const ConditionalPortal = usePortal
+ ? DropdownMenuPortal
+ : ({ children }: { children: React.ReactNode }) => <>{children}>;
return (
<>
{dialogs}
-
+
-
+
- {context === "booking-details-sheet" ? (
- menuContent
- ) : (
- {menuContent}
- )}
+
+
+
+ e.stopPropagation()}>
+ {t("view")}
+
+
+
+ {pendingActions.length > 0 && (
+ <>
+ {t("booking_response")}
+ {pendingActions.map((action) => (
+
+ {
+ e.stopPropagation();
+ action.onClick?.(e);
+ }}
+ data-bookingid={action.bookingId}
+ data-testid={action.id}
+ className={action.disabled ? "text-muted" : undefined}>
+ {action.label}
+
+
+ ))}
+
+ >
+ )}
+ {t("edit_event")}
+ {editEventActions.map((action) => (
+
+ {
+ e.stopPropagation();
+ action.onClick?.(e);
+ }}
+ data-bookingid={action.bookingId}
+ data-testid={action.id}
+ className={action.disabled ? "text-muted" : undefined}>
+ {action.label}
+
+
+ ))}
+
+ {t("after_event")}
+ {afterEventActions.map((action) => (
+
+ {
+ e.stopPropagation();
+ action.onClick?.(e);
+ }}
+ disabled={action.disabled}
+ data-bookingid={action.bookingId}
+ data-testid={action.id}
+ className={action.disabled ? "text-muted" : undefined}>
+ {action.label}
+
+
+ ))}
+ <>
+
+
+ {
+ e.stopPropagation();
+ reportActionWithHandler.onClick?.();
+ }}
+ disabled={reportActionWithHandler.disabled}
+ data-testid={reportActionWithHandler.id}
+ className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
+ {reportActionWithHandler.label}
+
+
+ >
+
+
+ {
+ e.stopPropagation();
+ cancelEventAction.onClick?.(e);
+ }}
+ disabled={cancelEventAction.disabled}
+ data-bookingid={cancelEventAction.bookingId}
+ data-testid={cancelEventAction.id}
+ className={cancelEventAction.disabled ? "text-muted" : undefined}>
+ {cancelEventAction.label}
+
+
+
+
>
);
diff --git a/apps/web/components/booking/actions/bookingActions.test.ts b/apps/web/components/booking/actions/bookingActions.test.ts
index 586add5f09..ec17b18531 100644
--- a/apps/web/components/booking/actions/bookingActions.test.ts
+++ b/apps/web/components/booking/actions/bookingActions.test.ts
@@ -137,18 +137,18 @@ describe("Booking Actions", () => {
expect(actions).toHaveLength(2);
expect(actions[0]).toEqual({
- id: "reject",
- label: "reject",
- icon: "ban",
- disabled: false,
- });
- expect(actions[1]).toEqual({
id: "confirm",
bookingId: 1,
label: "confirm",
icon: "check",
disabled: false,
});
+ expect(actions[1]).toEqual({
+ id: "reject",
+ label: "reject",
+ icon: "ban",
+ disabled: false,
+ });
});
it("should return reject action only for non-pending booking", () => {
@@ -167,8 +167,8 @@ describe("Booking Actions", () => {
});
const actions = getPendingActions(context);
- expect(actions[0].label).toBe("reject_all");
- expect(actions[1].label).toBe("confirm_all");
+ expect(actions[0].label).toBe("confirm_all");
+ expect(actions[1].label).toBe("reject_all");
});
});
diff --git a/apps/web/components/booking/actions/bookingActions.ts b/apps/web/components/booking/actions/bookingActions.ts
index da09095a89..4c3de355a7 100644
--- a/apps/web/components/booking/actions/bookingActions.ts
+++ b/apps/web/components/booking/actions/bookingActions.ts
@@ -38,14 +38,7 @@ export function getPendingActions(context: BookingActionContext): ActionType[] {
const { booking, isPending, isTabRecurring, isTabUnconfirmed, isRecurring, showPendingPayment, t } =
context;
- const actions: ActionType[] = [
- {
- id: "reject",
- label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
- icon: "ban",
- disabled: false, // This would be controlled by mutation state in the component
- },
- ];
+ const actions: ActionType[] = [];
// For bookings with payment, only confirm if the booking is paid for
// Original logic: (isPending && !paymentAppData.enabled) || (paymentAppData.enabled && !!paymentAppData.price && booking.paid)
@@ -59,6 +52,13 @@ export function getPendingActions(context: BookingActionContext): ActionType[] {
});
}
+ actions.push({
+ id: "reject",
+ label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
+ icon: "ban",
+ disabled: false, // This would be controlled by mutation state in the component
+ });
+
return actions;
}
diff --git a/apps/web/components/dialog/RescheduleDialog.tsx b/apps/web/components/dialog/RescheduleDialog.tsx
index 94dc20bb94..94171b8773 100644
--- a/apps/web/components/dialog/RescheduleDialog.tsx
+++ b/apps/web/components/dialog/RescheduleDialog.tsx
@@ -36,7 +36,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
return (