refactor: Booking list items actions UI (#22540)
* refactor: Booking list items actions UI * chore: save * fix: UI * fix: logic and remove unused * chore: change name * Update apps/web/components/booking/BookingListItem.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * perf: refactor * fix: update tests * chore: update reschedule test * refactor: create bookingActions and BookingListItem * tests: add unit tests for booking actions * fix: logic * fix: cancel button regresso --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent
ee9699e267
commit
16a7bb7bc5
@@ -18,7 +18,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
|
||||
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -41,8 +41,8 @@ import {
|
||||
import { TextAreaField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { MeetingTimeInTimezones } from "@calcom/ui/components/popover";
|
||||
import type { ActionType } from "@calcom/ui/components/table";
|
||||
import { TableActions } from "@calcom/ui/components/table";
|
||||
import type { ActionType } from "@calcom/ui/components/table";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
@@ -55,11 +55,22 @@ import { ReassignDialog } from "@components/dialog/ReassignDialog";
|
||||
import { RerouteDialog } from "@components/dialog/RerouteDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
import {
|
||||
getPendingActions,
|
||||
getCancelEventAction,
|
||||
getEditEventActions,
|
||||
getAfterEventActions,
|
||||
shouldShowPendingActions,
|
||||
shouldShowEditActions,
|
||||
shouldShowRecurringCancelAction,
|
||||
type BookingActionContext,
|
||||
} from "./bookingActions";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
type BookingItemProps = BookingItem & {
|
||||
export type BookingItemProps = BookingItem & {
|
||||
listingStatus: BookingListingStatus;
|
||||
recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
|
||||
loggedInUser: {
|
||||
@@ -191,6 +202,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
);
|
||||
const provider = guessEventLocationType(location);
|
||||
|
||||
const isDisabledCancelling = booking.eventType.disableCancelling;
|
||||
const isDisabledRescheduling = booking.eventType.disableRescheduling;
|
||||
|
||||
const bookingConfirm = async (confirm: boolean) => {
|
||||
let body = {
|
||||
bookingId: booking.id,
|
||||
@@ -214,174 +228,44 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
return booking.seatsReferences[0].referenceUid;
|
||||
};
|
||||
|
||||
const pendingActions: ActionType[] = [
|
||||
{
|
||||
id: "reject",
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: "ban",
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
...((isPending && !paymentAppData.enabled) ||
|
||||
(paymentAppData.enabled && !!paymentAppData.price && booking.paid)
|
||||
? [
|
||||
{
|
||||
id: "confirm",
|
||||
bookingId: booking.id,
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: "check" as const,
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const actionContext: BookingActionContext = {
|
||||
booking,
|
||||
isUpcoming,
|
||||
isOngoing,
|
||||
isBookingInPast,
|
||||
isCancelled,
|
||||
isConfirmed,
|
||||
isRejected,
|
||||
isPending,
|
||||
isRescheduled,
|
||||
isRecurring,
|
||||
isTabRecurring,
|
||||
isTabUnconfirmed,
|
||||
isBookingFromRoutingForm,
|
||||
isDisabledCancelling,
|
||||
isDisabledRescheduling,
|
||||
isCalVideoLocation:
|
||||
!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "",
|
||||
showPendingPayment: paymentAppData.enabled && booking.payment.length && !booking.paid,
|
||||
cardCharged,
|
||||
attendeeList,
|
||||
getSeatReferenceUid,
|
||||
t,
|
||||
} as BookingActionContext;
|
||||
|
||||
const editBookingActions: ActionType[] = [
|
||||
...(isBookingInPast && !booking.eventType.allowReschedulingPastBookings
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
]),
|
||||
...(isBookingFromRoutingForm
|
||||
? [
|
||||
{
|
||||
id: "reroute",
|
||||
label: t("reroute"),
|
||||
onClick: () => {
|
||||
setRerouteDialogIsOpen(true);
|
||||
},
|
||||
icon: "waypoints" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: "map-pin" as const,
|
||||
},
|
||||
...(booking.eventType?.disableGuests
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "add_members",
|
||||
label: t("additional_guests"),
|
||||
onClick: () => {
|
||||
setIsOpenAddGuestsDialog(true);
|
||||
},
|
||||
icon: "user-plus" as const,
|
||||
},
|
||||
]),
|
||||
];
|
||||
const basePendingActions = getPendingActions(actionContext);
|
||||
const pendingActions: ActionType[] = basePendingActions.map((action) => ({
|
||||
...action,
|
||||
onClick:
|
||||
action.id === "reject"
|
||||
? () => setRejectionDialogIsOpen(true)
|
||||
: action.id === "confirm"
|
||||
? () => bookingConfirm(true)
|
||||
: undefined,
|
||||
disabled: action.disabled || mutation.isPending,
|
||||
})) as ActionType[];
|
||||
|
||||
if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
editBookingActions.push({
|
||||
id: "reassign ",
|
||||
label: t("reassign"),
|
||||
onClick: () => {
|
||||
setIsOpenReassignDialog(true);
|
||||
},
|
||||
icon: "users" as const,
|
||||
});
|
||||
}
|
||||
|
||||
if (isBookingInPast || isOngoing) {
|
||||
editBookingActions.push({
|
||||
id: "no_show",
|
||||
label:
|
||||
attendeeList.length === 1 && attendeeList[0].noShow ? t("unmark_as_no_show") : t("mark_as_no_show"),
|
||||
onClick: () => {
|
||||
// If there's only one attendee, mark them as no-show directly without showing the dialog
|
||||
if (attendeeList.length === 1) {
|
||||
const attendee = attendeeList[0];
|
||||
noShowMutation.mutate({
|
||||
bookingUid: booking.uid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsNoShowDialogOpen(true);
|
||||
},
|
||||
icon: attendeeList.length === 1 && attendeeList[0].noShow ? "eye" : ("eye-off" as const),
|
||||
});
|
||||
}
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event"),
|
||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||
cancel all remaining bookings or just that booking instance. */
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||
`,
|
||||
icon: "x" as const,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
label: t("edit"),
|
||||
actions: editBookingActions,
|
||||
},
|
||||
];
|
||||
|
||||
const chargeCardActions: ActionType[] = [
|
||||
{
|
||||
id: "charge_card",
|
||||
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||
disabled: cardCharged,
|
||||
onClick: () => {
|
||||
setChargeCardDialogIsOpen(true);
|
||||
},
|
||||
icon: "credit-card" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const isDisabledCancelling = booking.eventType.disableCancelling;
|
||||
const isDisabledRescheduling = booking.eventType.disableRescheduling;
|
||||
|
||||
if (isTabRecurring && isRecurring) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
|
||||
if (isDisabledCancelling || (isBookingInPast && isPending && !isConfirmed)) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||
}
|
||||
|
||||
if (isDisabledRescheduling) {
|
||||
bookedActions.forEach((action) => {
|
||||
if (action.id === "edit_booking") {
|
||||
action.actions = action.actions?.filter(
|
||||
({ id }) => id !== "reschedule" && id !== "reschedule_request"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
const cancelEventAction = getCancelEventAction(actionContext);
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
@@ -460,38 +344,57 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
const title = booking.title;
|
||||
|
||||
const showViewRecordingsButton = !!(booking.isRecorded && isBookingInPast && isConfirmed);
|
||||
const showCheckRecordingButton =
|
||||
isBookingInPast &&
|
||||
isConfirmed &&
|
||||
!booking.isRecorded &&
|
||||
(!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
||||
|
||||
const isCalVideoLocation =
|
||||
!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "";
|
||||
|
||||
const videoOptionsActions: ActionType[] = [
|
||||
{
|
||||
id: "view_recordings",
|
||||
label: showCheckRecordingButton ? t("check_for_recordings") : t("view_recordings"),
|
||||
onClick: () => {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
color: showCheckRecordingButton ? "secondary" : "primary",
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
{
|
||||
id: "meeting_session_details",
|
||||
label: t("get_meeting_session_details"),
|
||||
onClick: () => {
|
||||
setMeetingSessionDetailsDialogIsOpen(true);
|
||||
},
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
];
|
||||
|
||||
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
||||
|
||||
const baseEditEventActions = getEditEventActions(actionContext);
|
||||
const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({
|
||||
...action,
|
||||
onClick:
|
||||
action.id === "reschedule_request"
|
||||
? () => setIsOpenRescheduleDialog(true)
|
||||
: action.id === "reroute"
|
||||
? () => setRerouteDialogIsOpen(true)
|
||||
: action.id === "change_location"
|
||||
? () => setIsOpenLocationDialog(true)
|
||||
: action.id === "add_members"
|
||||
? () => setIsOpenAddGuestsDialog(true)
|
||||
: action.id === "reassign"
|
||||
? () => setIsOpenReassignDialog(true)
|
||||
: undefined,
|
||||
})) as ActionType[];
|
||||
|
||||
const baseAfterEventActions = getAfterEventActions(actionContext);
|
||||
const afterEventActions: ActionType[] = baseAfterEventActions.map((action) => ({
|
||||
...action,
|
||||
onClick:
|
||||
action.id === "view_recordings"
|
||||
? () => setViewRecordingsDialogIsOpen(true)
|
||||
: action.id === "meeting_session_details"
|
||||
? () => setMeetingSessionDetailsDialogIsOpen(true)
|
||||
: action.id === "charge_card"
|
||||
? () => setChargeCardDialogIsOpen(true)
|
||||
: action.id === "no_show"
|
||||
? () => {
|
||||
if (attendeeList.length === 1) {
|
||||
const attendee = attendeeList[0];
|
||||
noShowMutation.mutate({
|
||||
bookingUid: booking.uid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsNoShowDialogOpen(true);
|
||||
}
|
||||
: undefined,
|
||||
disabled:
|
||||
action.disabled ||
|
||||
(action.id === "no_show" && !(isBookingInPast || isOngoing)) ||
|
||||
(action.id === "view_recordings" && !booking.isRecorded),
|
||||
})) as ActionType[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -726,32 +629,84 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</Link>
|
||||
</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">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{isPending && <TableActions actions={pendingActions} />}
|
||||
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||
</>
|
||||
) : null}
|
||||
{isBookingInPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{isBookingInPast && isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{isCalVideoLocation && (
|
||||
<TableActions
|
||||
actions={[{ id: "video_options", label: t("video_options"), actions: videoOptionsActions }]}
|
||||
/>
|
||||
{shouldShowPendingActions(actionContext) && <TableActions actions={pendingActions} />}
|
||||
{shouldShowEditActions(actionContext) && (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="ellipsis"
|
||||
data-testid="booking-actions-dropdown"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<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={cancelEventAction.id}
|
||||
disabled={cancelEventAction.disabled}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color={cancelEventAction.color}
|
||||
StartIcon={cancelEventAction.icon}
|
||||
href={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>
|
||||
</Dropdown>
|
||||
)}
|
||||
{shouldShowRecurringCancelAction(actionContext) && <TableActions actions={[cancelEventAction]} />}
|
||||
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="hidden h-full items-center md:flex">
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
{booking.status === "ACCEPTED" &&
|
||||
booking.paid &&
|
||||
booking.payment[0]?.paymentOption === "HOLD" && (
|
||||
<div className="ml-2">
|
||||
<TableActions actions={chargeCardActions} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<BookingItemBadges
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import {
|
||||
getPendingActions,
|
||||
getCancelEventAction,
|
||||
getVideoOptionsActions,
|
||||
getEditEventActions,
|
||||
getAfterEventActions,
|
||||
shouldShowPendingActions,
|
||||
shouldShowEditActions,
|
||||
shouldShowRecurringCancelAction,
|
||||
isActionDisabled,
|
||||
getActionLabel,
|
||||
type BookingActionContext,
|
||||
} from "./bookingActions";
|
||||
|
||||
const mockT = (key: string) => key;
|
||||
|
||||
function createMockContext(overrides: Partial<BookingActionContext> = {}): BookingActionContext {
|
||||
const now = new Date();
|
||||
const startTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Tomorrow
|
||||
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000); // 1 hour later
|
||||
|
||||
return {
|
||||
booking: {
|
||||
id: 1,
|
||||
uid: "booking-123",
|
||||
title: "Test Meeting",
|
||||
description: "Test meeting description",
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
paid: false,
|
||||
fromReschedule: null,
|
||||
recurringEventId: null,
|
||||
rescheduled: false,
|
||||
isRecorded: false,
|
||||
rescheduler: null,
|
||||
userPrimaryEmail: "organizer@example.com",
|
||||
customInputs: {},
|
||||
responses: {},
|
||||
references: [],
|
||||
attendees: [
|
||||
{
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
timeZone: "America/New_York",
|
||||
phoneNumber: null,
|
||||
locale: "en",
|
||||
bookingId: 1,
|
||||
noShow: false,
|
||||
} as any,
|
||||
],
|
||||
user: {
|
||||
id: 1,
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
},
|
||||
eventType: {
|
||||
id: 1,
|
||||
title: "Test Event Type",
|
||||
slug: "test-event",
|
||||
length: 60,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
team: null,
|
||||
eventTypeColor: {
|
||||
lightEventTypeColor: "#000000",
|
||||
darkEventTypeColor: "#ffffff",
|
||||
},
|
||||
disableCancelling: false,
|
||||
disableRescheduling: false,
|
||||
disableGuests: false,
|
||||
allowReschedulingPastBookings: false,
|
||||
recurringEvent: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
metadata: {},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
payment: [],
|
||||
seatsReferences: [],
|
||||
assignmentReason: [],
|
||||
metadata: null,
|
||||
routedFromRoutingFormReponse: null,
|
||||
listingStatus: "upcoming",
|
||||
recurringInfo: undefined,
|
||||
loggedInUser: {
|
||||
userId: 1,
|
||||
userTimeZone: "America/New_York",
|
||||
userTimeFormat: 12,
|
||||
userEmail: "organizer@example.com",
|
||||
},
|
||||
isToday: false,
|
||||
},
|
||||
isUpcoming: true,
|
||||
isOngoing: false,
|
||||
isBookingInPast: false,
|
||||
isCancelled: false,
|
||||
isConfirmed: true,
|
||||
isRejected: false,
|
||||
isPending: false,
|
||||
isRescheduled: false,
|
||||
isRecurring: false,
|
||||
isTabRecurring: false,
|
||||
isTabUnconfirmed: false,
|
||||
isBookingFromRoutingForm: false,
|
||||
isDisabledCancelling: false,
|
||||
isDisabledRescheduling: false,
|
||||
isCalVideoLocation: true,
|
||||
showPendingPayment: false,
|
||||
cardCharged: false,
|
||||
attendeeList: [
|
||||
{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
id: 1,
|
||||
noShow: false,
|
||||
phoneNumber: null,
|
||||
},
|
||||
],
|
||||
getSeatReferenceUid: () => undefined,
|
||||
t: mockT,
|
||||
...overrides,
|
||||
} as BookingActionContext;
|
||||
}
|
||||
|
||||
describe("Booking Actions", () => {
|
||||
describe("getPendingActions", () => {
|
||||
it("should return reject action for pending booking", () => {
|
||||
const context = createMockContext({ isPending: true });
|
||||
const actions = getPendingActions(context);
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return reject action only for non-pending booking", () => {
|
||||
const context = createMockContext({ isPending: false });
|
||||
const actions = getPendingActions(context);
|
||||
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0].id).toBe("reject");
|
||||
});
|
||||
|
||||
it("should use correct labels for recurring bookings", () => {
|
||||
const context = createMockContext({
|
||||
isPending: true,
|
||||
isRecurring: true,
|
||||
isTabRecurring: true,
|
||||
});
|
||||
const actions = getPendingActions(context);
|
||||
|
||||
expect(actions[0].label).toBe("reject_all");
|
||||
expect(actions[1].label).toBe("confirm_all");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCancelEventAction", () => {
|
||||
it("should return cancel action with correct properties", () => {
|
||||
const context = createMockContext();
|
||||
const action = getCancelEventAction(context);
|
||||
|
||||
expect(action).toEqual({
|
||||
id: "cancel",
|
||||
label: "cancel_event",
|
||||
href: "/booking/booking-123?cancel=true",
|
||||
icon: "circle-x",
|
||||
color: "destructive",
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should be disabled when cancellation is disabled", () => {
|
||||
const context = createMockContext({ isDisabledCancelling: true });
|
||||
const action = getCancelEventAction(context);
|
||||
|
||||
expect(action.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should be disabled for past pending bookings", () => {
|
||||
const context = createMockContext({
|
||||
isBookingInPast: true,
|
||||
isPending: true,
|
||||
isConfirmed: false,
|
||||
});
|
||||
const action = getCancelEventAction(context);
|
||||
|
||||
expect(action.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should include recurring parameters for recurring bookings", () => {
|
||||
const context = createMockContext({
|
||||
isRecurring: true,
|
||||
isTabRecurring: true,
|
||||
});
|
||||
const action = getCancelEventAction(context);
|
||||
|
||||
expect(action.href).toContain("allRemainingBookings=true");
|
||||
expect(action.label).toBe("cancel_all_remaining");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVideoOptionsActions", () => {
|
||||
it("should return video actions for past confirmed bookings", () => {
|
||||
const context = createMockContext({
|
||||
isBookingInPast: true,
|
||||
isConfirmed: true,
|
||||
isCalVideoLocation: true,
|
||||
booking: {
|
||||
...createMockContext().booking,
|
||||
isRecorded: true,
|
||||
},
|
||||
});
|
||||
const actions = getVideoOptionsActions(context);
|
||||
|
||||
expect(actions).toHaveLength(2);
|
||||
expect(actions[0].id).toBe("view_recordings");
|
||||
expect(actions[1].id).toBe("meeting_session_details");
|
||||
expect(actions[0].disabled).toBe(false);
|
||||
expect(actions[1].disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable video actions for upcoming bookings", () => {
|
||||
const context = createMockContext({ isBookingInPast: false });
|
||||
const actions = getVideoOptionsActions(context);
|
||||
|
||||
expect(actions[0].disabled).toBe(true);
|
||||
expect(actions[1].disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable video actions for non-Cal video locations", () => {
|
||||
const context = createMockContext({
|
||||
isBookingInPast: true,
|
||||
isConfirmed: true,
|
||||
isCalVideoLocation: false,
|
||||
});
|
||||
const actions = getVideoOptionsActions(context);
|
||||
|
||||
expect(actions[0].disabled).toBe(true);
|
||||
expect(actions[1].disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEditEventActions", () => {
|
||||
it("should return basic edit actions", () => {
|
||||
const context = createMockContext();
|
||||
const actions = getEditEventActions(context);
|
||||
|
||||
const actionIds = actions.map((a) => a.id);
|
||||
expect(actionIds).toContain("reschedule");
|
||||
expect(actionIds).toContain("reschedule_request");
|
||||
expect(actionIds).toContain("change_location");
|
||||
expect(actionIds).toContain("add_members");
|
||||
});
|
||||
|
||||
it("should include reroute action for routing form bookings", () => {
|
||||
const context = createMockContext({ isBookingFromRoutingForm: true });
|
||||
const actions = getEditEventActions(context);
|
||||
|
||||
const rerouteAction = actions.find((a) => a.id === "reroute");
|
||||
expect(rerouteAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("should include reassign action for round robin events", () => {
|
||||
const context = createMockContext({
|
||||
booking: {
|
||||
...createMockContext().booking,
|
||||
eventType: {
|
||||
...createMockContext().booking.eventType,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
},
|
||||
},
|
||||
});
|
||||
const actions = getEditEventActions(context);
|
||||
|
||||
const reassignAction = actions.find((a) => a.id === "reassign");
|
||||
expect(reassignAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("should exclude add_members when guests are disabled", () => {
|
||||
const context = createMockContext({
|
||||
booking: {
|
||||
...createMockContext().booking,
|
||||
eventType: {
|
||||
...createMockContext().booking.eventType,
|
||||
disableGuests: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const actions = getEditEventActions(context);
|
||||
|
||||
const addMembersAction = actions.find((a) => a.id === "add_members");
|
||||
expect(addMembersAction).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should disable reschedule actions when rescheduling is disabled", () => {
|
||||
const context = createMockContext({ isDisabledRescheduling: true });
|
||||
const actions = getEditEventActions(context);
|
||||
|
||||
const rescheduleAction = actions.find((a) => a.id === "reschedule");
|
||||
const rescheduleRequestAction = actions.find((a) => a.id === "reschedule_request");
|
||||
|
||||
expect(rescheduleAction?.disabled).toBe(true);
|
||||
expect(rescheduleRequestAction?.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAfterEventActions", () => {
|
||||
it("should include video actions and no-show action", () => {
|
||||
const context = createMockContext({ isBookingInPast: true, isConfirmed: true });
|
||||
const actions = getAfterEventActions(context);
|
||||
|
||||
const actionIds = actions.map((a) => a.id);
|
||||
expect(actionIds).toContain("view_recordings");
|
||||
expect(actionIds).toContain("meeting_session_details");
|
||||
expect(actionIds).toContain("no_show");
|
||||
});
|
||||
|
||||
it("should include charge card action for held payments", () => {
|
||||
const context = createMockContext({
|
||||
booking: {
|
||||
...createMockContext().booking,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
paid: true,
|
||||
payment: [{ paymentOption: "HOLD", amount: 1000, currency: "usd", success: true }],
|
||||
},
|
||||
});
|
||||
const actions = getAfterEventActions(context);
|
||||
|
||||
const chargeCardAction = actions.find((a) => a.id === "charge_card");
|
||||
expect(chargeCardAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("should show correct no-show label for single attendee", () => {
|
||||
const context = createMockContext({
|
||||
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
|
||||
});
|
||||
const actions = getAfterEventActions(context);
|
||||
|
||||
const noShowAction = actions.find((a) => a.id === "no_show");
|
||||
expect(noShowAction?.label).toBe("unmark_as_no_show");
|
||||
expect(noShowAction?.icon).toBe("eye");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldShowPendingActions", () => {
|
||||
it("should return true for pending upcoming bookings", () => {
|
||||
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: false });
|
||||
expect(shouldShowPendingActions(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for cancelled bookings", () => {
|
||||
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: true });
|
||||
expect(shouldShowPendingActions(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for past bookings", () => {
|
||||
const context = createMockContext({ isPending: true, isUpcoming: false, isCancelled: false });
|
||||
expect(shouldShowPendingActions(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldShowEditActions", () => {
|
||||
it("should return true for confirmed upcoming bookings", () => {
|
||||
const context = createMockContext({ isPending: false, isCancelled: false });
|
||||
expect(shouldShowEditActions(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for pending bookings", () => {
|
||||
const context = createMockContext({ isPending: true });
|
||||
expect(shouldShowEditActions(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for cancelled bookings", () => {
|
||||
const context = createMockContext({ isCancelled: true });
|
||||
expect(shouldShowEditActions(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldShowRecurringCancelAction", () => {
|
||||
it("should return true for recurring bookings in recurring tab", () => {
|
||||
const context = createMockContext({ isTabRecurring: true, isRecurring: true });
|
||||
expect(shouldShowRecurringCancelAction(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-recurring bookings", () => {
|
||||
const context = createMockContext({ isTabRecurring: true, isRecurring: false });
|
||||
expect(shouldShowRecurringCancelAction(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isActionDisabled", () => {
|
||||
it("should disable reschedule actions when rescheduling is disabled", () => {
|
||||
const context = createMockContext({ isDisabledRescheduling: true });
|
||||
|
||||
expect(isActionDisabled("reschedule", context)).toBe(true);
|
||||
expect(isActionDisabled("reschedule_request", context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable cancel action when cancellation is disabled", () => {
|
||||
const context = createMockContext({ isDisabledCancelling: true });
|
||||
|
||||
expect(isActionDisabled("cancel", context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable video actions for non-past bookings", () => {
|
||||
const context = createMockContext({ isBookingInPast: false });
|
||||
|
||||
expect(isActionDisabled("view_recordings", context)).toBe(true);
|
||||
expect(isActionDisabled("meeting_session_details", context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable charge card action when already charged", () => {
|
||||
const context = createMockContext({ cardCharged: true });
|
||||
|
||||
expect(isActionDisabled("charge_card", context)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActionLabel", () => {
|
||||
it("should return correct labels for different actions", () => {
|
||||
const context = createMockContext();
|
||||
|
||||
expect(getActionLabel("reject", context)).toBe("reject");
|
||||
expect(getActionLabel("confirm", context)).toBe("confirm");
|
||||
expect(getActionLabel("cancel", context)).toBe("cancel_event");
|
||||
});
|
||||
|
||||
it("should return correct labels for recurring bookings", () => {
|
||||
const context = createMockContext({ isRecurring: true, isTabRecurring: true });
|
||||
|
||||
expect(getActionLabel("reject", context)).toBe("reject_all");
|
||||
expect(getActionLabel("confirm", context)).toBe("confirm_all");
|
||||
expect(getActionLabel("cancel", context)).toBe("cancel_all_remaining");
|
||||
});
|
||||
|
||||
it("should return correct no-show label based on attendee state", () => {
|
||||
const contextWithNoShow = createMockContext({
|
||||
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
|
||||
});
|
||||
|
||||
expect(getActionLabel("no_show", contextWithNoShow)).toBe("unmark_as_no_show");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { ActionType } from "@calcom/ui/components/table";
|
||||
|
||||
import type { BookingItemProps } from "./BookingListItem";
|
||||
|
||||
export interface BookingActionContext {
|
||||
booking: BookingItemProps;
|
||||
isUpcoming: boolean;
|
||||
isOngoing: boolean;
|
||||
isBookingInPast: boolean;
|
||||
isCancelled: boolean;
|
||||
isConfirmed: boolean;
|
||||
isRejected: boolean;
|
||||
isPending: boolean;
|
||||
isRescheduled: boolean;
|
||||
isRecurring: boolean;
|
||||
isTabRecurring: boolean;
|
||||
isTabUnconfirmed: boolean;
|
||||
isBookingFromRoutingForm: boolean;
|
||||
isDisabledCancelling: boolean;
|
||||
isDisabledRescheduling: boolean;
|
||||
isCalVideoLocation: boolean;
|
||||
showPendingPayment: boolean;
|
||||
cardCharged: boolean;
|
||||
attendeeList: Array<{
|
||||
name: string;
|
||||
email: string;
|
||||
id: number;
|
||||
noShow: boolean;
|
||||
phoneNumber: string | null;
|
||||
}>;
|
||||
getSeatReferenceUid: () => string | undefined;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
];
|
||||
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
// Original logic: (isPending && !paymentAppData.enabled) || (paymentAppData.enabled && !!paymentAppData.price && booking.paid)
|
||||
if ((isPending && !showPendingPayment) || (showPendingPayment && booking.paid)) {
|
||||
actions.push({
|
||||
id: "confirm",
|
||||
bookingId: booking.id,
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
icon: "check" as const,
|
||||
disabled: false, // This would be controlled by mutation state in the component
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function getCancelEventAction(context: BookingActionContext): ActionType {
|
||||
const { booking, isTabRecurring, isRecurring, getSeatReferenceUid, t } = context;
|
||||
|
||||
return {
|
||||
id: "cancel",
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event"),
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}`,
|
||||
icon: "circle-x",
|
||||
color: "destructive",
|
||||
disabled: isActionDisabled("cancel", context),
|
||||
};
|
||||
}
|
||||
|
||||
export function getVideoOptionsActions(context: BookingActionContext): ActionType[] {
|
||||
const { booking, isBookingInPast, isConfirmed, isCalVideoLocation, t } = context;
|
||||
|
||||
return [
|
||||
{
|
||||
id: "view_recordings",
|
||||
label: t("view_recordings"),
|
||||
icon: "video",
|
||||
disabled: !(isBookingInPast && isConfirmed && isCalVideoLocation && booking.isRecorded),
|
||||
},
|
||||
{
|
||||
id: "meeting_session_details",
|
||||
label: t("view_session_details"),
|
||||
icon: "info",
|
||||
disabled: !(isBookingInPast && isConfirmed && isCalVideoLocation),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getEditEventActions(context: BookingActionContext): ActionType[] {
|
||||
const {
|
||||
booking,
|
||||
isBookingInPast,
|
||||
isDisabledRescheduling,
|
||||
isBookingFromRoutingForm,
|
||||
getSeatReferenceUid,
|
||||
t,
|
||||
} = context;
|
||||
|
||||
const actions: (ActionType | null)[] = [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: "clock",
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
disabled:
|
||||
(isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: "send",
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
disabled:
|
||||
(isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling,
|
||||
},
|
||||
isBookingFromRoutingForm
|
||||
? {
|
||||
id: "reroute",
|
||||
label: t("reroute"),
|
||||
icon: "waypoints",
|
||||
disabled: false,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
icon: "map-pin",
|
||||
disabled: false,
|
||||
},
|
||||
booking.eventType?.disableGuests
|
||||
? null
|
||||
: {
|
||||
id: "add_members",
|
||||
label: t("additional_guests"),
|
||||
icon: "user-plus",
|
||||
disabled: false,
|
||||
},
|
||||
// Reassign (if round robin)
|
||||
booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||
? {
|
||||
id: "reassign",
|
||||
label: t("reassign"),
|
||||
icon: "users",
|
||||
disabled: false,
|
||||
}
|
||||
: null,
|
||||
];
|
||||
|
||||
return actions.filter(Boolean) as ActionType[];
|
||||
}
|
||||
|
||||
export function getAfterEventActions(context: BookingActionContext): ActionType[] {
|
||||
const { booking, cardCharged, attendeeList, t } = context;
|
||||
|
||||
const actions: (ActionType | null)[] = [
|
||||
...getVideoOptionsActions(context),
|
||||
booking.status === BookingStatus.ACCEPTED && booking.paid && booking.payment[0]?.paymentOption === "HOLD"
|
||||
? {
|
||||
id: "charge_card",
|
||||
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||
icon: "credit-card",
|
||||
disabled: cardCharged,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
id: "no_show",
|
||||
label:
|
||||
attendeeList.length === 1 && attendeeList[0].noShow ? t("unmark_as_no_show") : t("mark_as_no_show"),
|
||||
icon: attendeeList.length === 1 && attendeeList[0].noShow ? "eye" : "eye-off",
|
||||
disabled: false, // This would be controlled by booking state in the component
|
||||
},
|
||||
];
|
||||
|
||||
return actions.filter(Boolean) as ActionType[];
|
||||
}
|
||||
|
||||
export function shouldShowPendingActions(context: BookingActionContext): boolean {
|
||||
const { isPending, isUpcoming, isCancelled } = context;
|
||||
return isPending && isUpcoming && !isCancelled;
|
||||
}
|
||||
|
||||
export function shouldShowEditActions(context: BookingActionContext): boolean {
|
||||
const { isPending, isTabRecurring, isRecurring, isCancelled } = context;
|
||||
return !isPending && !(isTabRecurring && isRecurring) && !isCancelled;
|
||||
}
|
||||
|
||||
export function shouldShowRecurringCancelAction(context: BookingActionContext): boolean {
|
||||
const { isTabRecurring, isRecurring } = context;
|
||||
return isTabRecurring && isRecurring;
|
||||
}
|
||||
|
||||
export function isActionDisabled(actionId: string, context: BookingActionContext): boolean {
|
||||
const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isPending, isConfirmed } =
|
||||
context;
|
||||
|
||||
switch (actionId) {
|
||||
case "reschedule":
|
||||
case "reschedule_request":
|
||||
return (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling;
|
||||
case "cancel":
|
||||
return isDisabledCancelling || (isBookingInPast && isPending && !isConfirmed);
|
||||
case "view_recordings":
|
||||
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
|
||||
case "meeting_session_details":
|
||||
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
|
||||
case "charge_card":
|
||||
return context.cardCharged;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getActionLabel(actionId: string, context: BookingActionContext): string {
|
||||
const { booking, isTabRecurring, isRecurring, attendeeList, cardCharged, t } = context;
|
||||
|
||||
switch (actionId) {
|
||||
case "reject":
|
||||
return (isTabRecurring || context.isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject");
|
||||
case "confirm":
|
||||
return (isTabRecurring || context.isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm");
|
||||
case "cancel":
|
||||
return isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event");
|
||||
case "no_show":
|
||||
return attendeeList.length === 1 && attendeeList[0].noShow
|
||||
? t("unmark_as_no_show")
|
||||
: t("mark_as_no_show");
|
||||
case "charge_card":
|
||||
return cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee");
|
||||
default:
|
||||
return t(actionId);
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,8 @@ test.describe("pro user", () => {
|
||||
await pro.apiLogin();
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.waitForSelector('[data-testid="bookings"]');
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
await page.locator('[data-testid="reschedule"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
const bookingId = url.searchParams.get("rescheduleUid");
|
||||
@@ -205,6 +206,9 @@ test.describe("pro user", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
// Click the cancel option in the dropdown
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking/");
|
||||
@@ -237,6 +241,9 @@ test.describe("pro user", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
// Click the cancel option in the dropdown
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking/");
|
||||
|
||||
@@ -363,7 +363,10 @@ test.describe("Bookings", () => {
|
||||
.locator(`[data-testid="select-filter-options-userId"] [role="option"]:has-text("${thirdUser.name}")`)
|
||||
.click();
|
||||
await bookingsGetResponse2;
|
||||
await expect(page.locator('text="Cancel event"').nth(0)).toBeVisible();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
// Check that the cancel option is visible in the dropdown
|
||||
await expect(page.locator('[data-testid="cancel"]')).toBeVisible();
|
||||
|
||||
//expect only 3 bookings (out of 4 total) to be shown in list.
|
||||
//where ThirdUser is either organizer or attendee
|
||||
|
||||
@@ -36,7 +36,9 @@ test("dynamic booking", async ({ page, users }) => {
|
||||
await test.step("can reschedule a booking", async () => {
|
||||
// Logged in
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
// Click the reschedule option in the dropdown
|
||||
await page.locator('[data-testid="reschedule"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
const bookingId = url.searchParams.get("rescheduleUid");
|
||||
@@ -54,6 +56,9 @@ test("dynamic booking", async ({ page, users }) => {
|
||||
|
||||
await test.step("Can cancel the recently created booking", async () => {
|
||||
await page.goto("/bookings/upcoming");
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
// Click the cancel option in the dropdown
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking");
|
||||
|
||||
@@ -33,7 +33,8 @@ test.describe("Reschedule Tests", async () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/bookings/upcoming");
|
||||
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
|
||||
await page.locator('[data-testid="reschedule_request"]').click();
|
||||
|
||||
@@ -75,7 +76,8 @@ test.describe("Reschedule Tests", async () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/bookings/past");
|
||||
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
|
||||
await expect(page.locator('[data-testid="reschedule"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="reschedule_request"]')).toBeVisible();
|
||||
@@ -91,10 +93,14 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await page.reload();
|
||||
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
// Click the ellipsis menu button to open the dropdown
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
|
||||
await expect(page.locator('[data-testid="reschedule"]')).toBeHidden();
|
||||
await expect(page.locator('[data-testid="reschedule_request"]')).toBeHidden();
|
||||
// Check that the reschedule options are visible but disabled
|
||||
await expect(page.locator('[data-testid="reschedule"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="reschedule_request"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="reschedule"]')).toBeDisabled();
|
||||
await expect(page.locator('[data-testid="reschedule_request"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Should display former time when rescheduling availability", async ({ page, users, bookings }) => {
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"enable_automatic_transcription": "Enable automatic transcription after joining the meeting",
|
||||
"enable_automatic_recording": "Enable automatic recording after organizer joins the meeting",
|
||||
"video_options": "Video Options",
|
||||
"get_meeting_session_details": "Get Meeting Session Details",
|
||||
"edit_event": "Edit event",
|
||||
"view_session_details": "View Session Details",
|
||||
"meeting_session_details": "Meeting Session Details",
|
||||
"meeting_session": "Meeting Session",
|
||||
"session_id": "Session ID",
|
||||
|
||||
Reference in New Issue
Block a user