feat: add booking actions into BookingDetailsSheet (#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
This commit is contained in:
@@ -46,8 +46,11 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
|
||||
|
||||
import { buildBookingLink } from "../../modules/bookings/lib/buildBookingLink";
|
||||
import { BookingActionsDropdown } from "./BookingActionsDropdown";
|
||||
import { useBookingActionsStoreContext, BookingActionsStoreProvider } from "./BookingActionsStoreProvider";
|
||||
import { BookingActionsDropdown } from "./actions/BookingActionsDropdown";
|
||||
import {
|
||||
useBookingActionsStoreContext,
|
||||
BookingActionsStoreProvider,
|
||||
} from "./actions/BookingActionsStoreProvider";
|
||||
import {
|
||||
getPendingActions,
|
||||
getCancelEventAction,
|
||||
@@ -56,7 +59,7 @@ import {
|
||||
shouldShowIndividualReportButton,
|
||||
type BookingActionContext,
|
||||
getReportAction,
|
||||
} from "./bookingActions";
|
||||
} from "./actions/bookingActions";
|
||||
import type { BookingItemProps } from "./types";
|
||||
|
||||
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
|
||||
@@ -496,7 +499,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</div>
|
||||
<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">
|
||||
{shouldShowPendingActions(actionContext) && <TableActions actions={pendingActions} />}
|
||||
<BookingActionsDropdown booking={booking} />
|
||||
<BookingActionsDropdown booking={booking} context="booking-list-item" />
|
||||
{shouldShowRecurringCancelAction(actionContext) && <TableActions actions={[cancelEventAction]} />}
|
||||
{shouldShowIndividualReportButton(actionContext) && (
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
+112
-88
@@ -31,6 +31,7 @@ import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
|
||||
import { RerouteDialog } from "@components/dialog/RerouteDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
import type { BookingItemProps } from "../types";
|
||||
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
|
||||
import {
|
||||
getCancelEventAction,
|
||||
@@ -40,13 +41,14 @@ import {
|
||||
shouldShowEditActions,
|
||||
type BookingActionContext,
|
||||
} from "./bookingActions";
|
||||
import type { BookingItemProps } from "./types";
|
||||
|
||||
interface BookingActionsDropdownProps {
|
||||
booking: BookingItemProps;
|
||||
context: "booking-list-item" | "booking-details-sheet";
|
||||
size?: "xs" | "sm" | "base" | "lg";
|
||||
}
|
||||
|
||||
export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps) {
|
||||
export function BookingActionsDropdown({ booking, context, size = "base" }: BookingActionsDropdownProps) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
@@ -236,9 +238,11 @@ export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps)
|
||||
|
||||
const cancelEventAction = getCancelEventAction(actionContext);
|
||||
|
||||
const shouldShowEdit = shouldShowEditActions(actionContext);
|
||||
const baseEditEventActions = getEditEventActions(actionContext);
|
||||
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
|
||||
...action,
|
||||
disabled: !shouldShowEdit || action.disabled, // Disable all edit actions if shouldn't show edit actions
|
||||
onClick:
|
||||
action.id === "reschedule_request"
|
||||
? () => setIsOpenRescheduleDialog(true)
|
||||
@@ -494,99 +498,119 @@ export function BookingActionsDropdown({ booking }: BookingActionsDropdownProps)
|
||||
</>
|
||||
);
|
||||
|
||||
// Don't render dropdown if edit actions shouldn't be shown
|
||||
if (!shouldShowEditActions(actionContext)) {
|
||||
// Check if there are any available actions across all action groups
|
||||
const hasAnyAvailableActions = () => {
|
||||
// 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
|
||||
const isReportAvailable = !reportActionWithHandler.disabled;
|
||||
const isCancelAvailable = !cancelEventAction.disabled;
|
||||
|
||||
return hasAvailableEditAction || hasAvailableAfterAction || isReportAvailable || isCancelAvailable;
|
||||
};
|
||||
|
||||
// Don't render dropdown if no actions are available
|
||||
if (!hasAnyAvailableActions()) {
|
||||
return dialogs;
|
||||
}
|
||||
|
||||
const menuContent = (
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("edit_event")}</DropdownMenuLabel>
|
||||
{editEventActions.map((action) => (
|
||||
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={action.color}
|
||||
StartIcon={action.icon}
|
||||
href={action.href}
|
||||
disabled={action.disabled}
|
||||
onClick={action.onClick}
|
||||
data-bookingid={action.bookingId}
|
||||
data-testid={action.id}
|
||||
className={action.disabled ? "text-muted" : undefined}>
|
||||
{action.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">{t("after_event")}</DropdownMenuLabel>
|
||||
{afterEventActions.map((action) => (
|
||||
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={action.color}
|
||||
StartIcon={action.icon}
|
||||
href={action.href}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
data-bookingid={action.bookingId}
|
||||
data-testid={action.id}
|
||||
className={action.disabled ? "text-muted" : undefined}>
|
||||
{action.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="rounded-lg"
|
||||
key={reportActionWithHandler.id}
|
||||
disabled={reportActionWithHandler.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={reportActionWithHandler.color}
|
||||
StartIcon={reportActionWithHandler.icon}
|
||||
onClick={reportActionWithHandler.onClick}
|
||||
disabled={reportActionWithHandler.disabled}
|
||||
data-testid={reportActionWithHandler.id}
|
||||
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
|
||||
{reportActionWithHandler.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="rounded-lg"
|
||||
key={cancelEventAction.id}
|
||||
disabled={cancelEventAction.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={cancelEventAction.color}
|
||||
StartIcon={cancelEventAction.icon}
|
||||
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
|
||||
onClick={cancelEventAction.onClick}
|
||||
disabled={cancelEventAction.disabled}
|
||||
data-bookingid={cancelEventAction.bookingId}
|
||||
data-testid={cancelEventAction.id}
|
||||
className={cancelEventAction.disabled ? "text-muted" : undefined}>
|
||||
{cancelEventAction.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{dialogs}
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="ellipsis"
|
||||
data-testid="booking-actions-dropdown"
|
||||
/>
|
||||
<Dropdown modal={context === "booking-details-sheet" ? false : undefined}>
|
||||
<DropdownMenuTrigger asChild data-testid="booking-actions-dropdown">
|
||||
<Button type="button" color="secondary" variant="icon" size={size} StartIcon="ellipsis" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className="p-2">{t("edit_event")}</DropdownMenuLabel>
|
||||
{editEventActions.map((action) => (
|
||||
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={action.color}
|
||||
StartIcon={action.icon}
|
||||
href={action.href}
|
||||
disabled={action.disabled}
|
||||
onClick={action.onClick}
|
||||
data-bookingid={action.bookingId}
|
||||
data-testid={action.id}
|
||||
className={action.disabled ? "text-muted" : undefined}>
|
||||
{action.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator className="!my-1 bg-subtle" />
|
||||
<DropdownMenuLabel className="p-2">{t("after_event")}</DropdownMenuLabel>
|
||||
{afterEventActions.map((action) => (
|
||||
<DropdownMenuItem className="rounded-lg" key={action.id} disabled={action.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={action.color}
|
||||
StartIcon={action.icon}
|
||||
href={action.href}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
data-bookingid={action.bookingId}
|
||||
data-testid={action.id}
|
||||
className={action.disabled ? "text-muted" : undefined}>
|
||||
{action.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<>
|
||||
<DropdownMenuSeparator className="!my-1 bg-subtle" />
|
||||
<DropdownMenuItem
|
||||
className="rounded-lg"
|
||||
key={reportActionWithHandler.id}
|
||||
disabled={reportActionWithHandler.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={reportActionWithHandler.color}
|
||||
StartIcon={reportActionWithHandler.icon}
|
||||
onClick={reportActionWithHandler.onClick}
|
||||
disabled={reportActionWithHandler.disabled}
|
||||
data-testid={reportActionWithHandler.id}
|
||||
className={reportActionWithHandler.disabled ? "text-muted" : undefined}>
|
||||
{reportActionWithHandler.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
<DropdownMenuSeparator className="!my-1 bg-subtle" />
|
||||
<DropdownMenuItem
|
||||
className="rounded-lg"
|
||||
key={cancelEventAction.id}
|
||||
disabled={cancelEventAction.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={cancelEventAction.color}
|
||||
StartIcon={cancelEventAction.icon}
|
||||
href={cancelEventAction.disabled ? undefined : cancelEventAction.href}
|
||||
onClick={cancelEventAction.onClick}
|
||||
disabled={cancelEventAction.disabled}
|
||||
data-bookingid={cancelEventAction.bookingId}
|
||||
data-testid={cancelEventAction.id}
|
||||
className={cancelEventAction.disabled ? "text-muted" : undefined}>
|
||||
{cancelEventAction.label}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
{context === "booking-details-sheet" ? (
|
||||
menuContent
|
||||
) : (
|
||||
<DropdownMenuPortal>{menuContent}</DropdownMenuPortal>
|
||||
)}
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
+1
-1
@@ -54,7 +54,7 @@ function createMockContext(overrides: Partial<BookingActionContext> = {}): Booki
|
||||
locale: "en",
|
||||
bookingId: 1,
|
||||
noShow: false,
|
||||
} as any,
|
||||
},
|
||||
],
|
||||
user: {
|
||||
id: 1,
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { ActionType } from "@calcom/ui/components/table";
|
||||
|
||||
import type { BookingItemProps } from "./types";
|
||||
import type { BookingItemProps } from "../types";
|
||||
|
||||
export interface BookingActionContext {
|
||||
booking: BookingItemProps;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
export type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
SheetTitle,
|
||||
} from "@calcom/ui/components/sheet";
|
||||
|
||||
import { BookingActionsDropdown } from "../../../components/booking/actions/BookingActionsDropdown";
|
||||
import { BookingActionsStoreProvider } from "../../../components/booking/actions/BookingActionsStoreProvider";
|
||||
import type { BookingListingStatus } from "../../../components/booking/types";
|
||||
import { buildBookingLink } from "../lib/buildBookingLink";
|
||||
import type { BookingOutput } from "../types";
|
||||
|
||||
@@ -37,26 +40,43 @@ interface BookingDetailsSheetProps {
|
||||
onClose: () => void;
|
||||
userTimeZone?: string;
|
||||
userTimeFormat?: number;
|
||||
userId?: number;
|
||||
userEmail?: string;
|
||||
onPrevious?: () => void;
|
||||
hasPrevious?: boolean;
|
||||
onNext?: () => void;
|
||||
hasNext?: boolean;
|
||||
}
|
||||
|
||||
export function BookingDetailsSheet({
|
||||
interface BookingDetailsSheetInnerProps extends Omit<BookingDetailsSheetProps, "booking"> {
|
||||
booking: BookingOutput;
|
||||
}
|
||||
|
||||
export function BookingDetailsSheet(props: BookingDetailsSheetProps) {
|
||||
if (!props.booking) return null;
|
||||
|
||||
return (
|
||||
<BookingActionsStoreProvider>
|
||||
<BookingDetailsSheetInner {...props} booking={props.booking} />
|
||||
</BookingActionsStoreProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function BookingDetailsSheetInner({
|
||||
booking,
|
||||
isOpen,
|
||||
onClose,
|
||||
userTimeZone,
|
||||
userTimeFormat,
|
||||
userId,
|
||||
userEmail,
|
||||
onPrevious,
|
||||
hasPrevious = false,
|
||||
onNext,
|
||||
hasNext = false,
|
||||
}: BookingDetailsSheetProps) {
|
||||
}: BookingDetailsSheetInnerProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!booking) return null;
|
||||
|
||||
const startTime = dayjs(booking.startTime).tz(userTimeZone);
|
||||
const endTime = dayjs(booking.endTime).tz(userTimeZone);
|
||||
|
||||
@@ -210,6 +230,22 @@ export function BookingDetailsSheet({
|
||||
<Button color="secondary" size="sm" EndIcon="external-link" href={bookingLink} target="_blank">
|
||||
{t("view")}
|
||||
</Button>
|
||||
<BookingActionsDropdown
|
||||
context="booking-details-sheet"
|
||||
size="sm"
|
||||
booking={{
|
||||
...booking,
|
||||
listingStatus: booking.status.toLowerCase() as BookingListingStatus,
|
||||
recurringInfo: undefined,
|
||||
loggedInUser: {
|
||||
userId,
|
||||
userTimeZone,
|
||||
userTimeFormat: userTimeFormat ?? null,
|
||||
userEmail,
|
||||
},
|
||||
isToday: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
@@ -473,6 +473,8 @@ function BookingsContent({ status, permissions, isCalendarViewEnabled }: Booking
|
||||
onClose={() => setSelectedBookingId(null)}
|
||||
userTimeZone={user?.timeZone}
|
||||
userTimeFormat={user?.timeFormat === null ? undefined : user?.timeFormat}
|
||||
userId={user?.id}
|
||||
userEmail={user?.email}
|
||||
onPrevious={bookingNavigation.onPrevious}
|
||||
hasPrevious={bookingNavigation.hasPrevious}
|
||||
onNext={bookingNavigation.onNext}
|
||||
|
||||
@@ -3968,5 +3968,6 @@
|
||||
"team_invite_subtitle": "Invite team members to collaborate and schedule together",
|
||||
"team_onboarding_details_subtitle": "Add your team's name and create a unique URL for your team",
|
||||
"view_booking_details": "View Booking Details",
|
||||
"view": "View",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user