fix: handle cancelation reason when user is host (#18681)

* handle cancelation reason when user is host

* add attendee reason warning

* add cancellation reason to tests when we are the host

* req.body and not req

* fix: isloggedIn logic

* use new isLoggedInUser from ssr instead of client

* update e2e tests to fill in reason when hosts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
sean-brydon
2025-01-21 20:24:06 +05:30
committed by GitHub
co-authored by Udit Takkar Udit Takkar Peer Richelsen
parent 1a1a506324
commit 7a9d291d1f
9 changed files with 53 additions and 8 deletions
+12 -2
View File
@@ -5,7 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRefreshData } from "@calcom/lib/hooks/useRefreshData";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Icon, TextArea } from "@calcom/ui";
import { Button, Icon, Label, TextArea } from "@calcom/ui";
type Props = {
booking: {
@@ -33,6 +33,7 @@ type Props = {
};
eventType: unknown;
};
isHost: boolean;
};
export default function CancelBooking(props: Props) {
@@ -70,7 +71,7 @@ export default function CancelBooking(props: Props) {
)}
{!error && (
<div className="mt-5 sm:mt-6">
<label className="text-default font-medium">{t("cancellation_reason")}</label>
<Label>{props.isHost ? t("cancellation_reason_host") : t("cancellation_reason")}</Label>
<TextArea
data-testid="cancel_reason"
ref={cancelBookingRef}
@@ -80,6 +81,14 @@ export default function CancelBooking(props: Props) {
className="mb-4 mt-2 w-full "
rows={3}
/>
{props.isHost ? (
<div className="-mt-2 mb-4 flex items-center gap-2">
<Icon name="info" className="text-subtle h-4 w-4" />
<p className="text-default text-subtle text-sm leading-none">
{t("notify_attendee_cancellation_reason_warning")}
</p>
</div>
) : null}
<div className="flex flex-col-reverse rtl:space-x-reverse ">
<div className="ml-auto flex w-full space-x-4 ">
<Button
@@ -90,6 +99,7 @@ export default function CancelBooking(props: Props) {
</Button>
<Button
data-testid="confirm_cancel"
disabled={props.isHost && !cancellationReason}
onClick={async () => {
setLoading(true);
@@ -163,10 +163,24 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
const userId = session?.user?.id;
const isLoggedInUserHost =
userId &&
(eventType.users.some((user) => user.id === userId) ||
eventType.hosts.some(({ user }) => user.id === userId));
const checkIfUserIsHost = (userId?: number | null) => {
if (!userId) return false;
return (
bookingInfo?.user?.id === userId ||
eventType.users.some(
(user) =>
user.id === userId && bookingInfo.attendees.some((attendee) => attendee.email === user.email)
) ||
eventType.hosts.some(
({ user }) =>
user.id === userId && bookingInfo.attendees.some((attendee) => attendee.email === user.email)
)
);
};
const isLoggedInUserHost = checkIfUserIsHost(userId);
if (!isLoggedInUserHost) {
// Removing hidden fields from responses
@@ -195,6 +209,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
userTimeFormat,
requiresLoginToUpdate,
rescheduledToUid,
isLoggedInUserHost,
},
};
}
@@ -132,7 +132,6 @@ export default function Success(props: PageProps) {
) {
rescheduleLocation = bookingInfo.responses.location.optionValue;
}
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
bookingInfo?.metadata || {}
)?.videoCallUrl;
@@ -149,6 +148,7 @@ export default function Success(props: PageProps) {
props?.userTimeFormat ? props.userTimeFormat === 24 : isBrowserLocale24h()
);
const { data: session } = useSession();
const isHost = props.isLoggedInUserHost;
const [date, setDate] = useState(dayjs.utc(bookingInfo.startTime));
@@ -317,7 +317,6 @@ export default function Success(props: PageProps) {
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
}
if (bookingInfo.user) {
const isHost = bookingInfo.user.id === session?.user?.id;
const isAttendee = bookingInfo.attendees.find((attendee) => attendee.email === session?.user?.email);
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
const host = bookingInfo.user.name || bookingInfo.user.email;
@@ -778,6 +777,7 @@ export default function Success(props: PageProps) {
seatReferenceUid={seatReferenceUid}
bookingCancelledEventProps={bookingCancelledEventProps}
currentUserEmail={currentUserEmail}
isHost={isHost}
/>
</>
))}
+3
View File
@@ -175,6 +175,8 @@ test.describe("pro user", () => {
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking/");
});
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
await page.locator('[data-testid="confirm_cancel"]').click();
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
@@ -205,6 +207,7 @@ test.describe("pro user", () => {
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking/");
});
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
await page.locator('[data-testid="confirm_cancel"]').click();
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
+4
View File
@@ -228,6 +228,8 @@ test.describe("Reschedule for booking with seats", () => {
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}`
);
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
await page.locator('[data-testid="confirm_cancel"]').click();
await expect(page.locator("text=You are no longer attending this event")).toBeVisible();
@@ -236,6 +238,8 @@ test.describe("Reschedule for booking with seats", () => {
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}`
);
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
// Page should not be 404
await page.locator('[data-testid="confirm_cancel"]').click();
@@ -56,6 +56,7 @@ test("dynamic booking", async ({ page, users }) => {
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
await page.locator('[data-testid="confirm_cancel"]').click();
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
@@ -52,7 +52,9 @@
"you_can_view_booking_details_with_this_url": "You can view the booking details from this url {{url}} and add the event to your calendar",
"no_options_available": "No options available",
"cancellation_reason": "Reason for cancellation (optional)",
"cancellation_reason_host": "Reason for cancellation",
"cancellation_reason_placeholder": "Why are you cancelling?",
"notify_attendee_cancellation_reason_warning":"Please note that the cancellation reason will be visible to the attendee.",
"rejection_reason_placeholder": "Why are you rejecting?",
"rejection_reason": "Reason for rejecting",
"rejection_reason_title": "Reject the booking request?",
@@ -180,6 +180,13 @@ async function handler(req: CustomRequest) {
throw new HttpError({ statusCode: 400, message: "User not found" });
}
if (!cancellationReason && req.bookingToDelete.userId == userId) {
throw new HttpError({
statusCode: 400,
message: "Cancellation reason is required when you are the host",
});
}
// If the booking is a seated event and there is no seatReferenceUid we should validate that logged in user is host
if (bookingToDelete.eventType?.seatsPerTimeSlot && !seatReferenceUid) {
const userIsHost = bookingToDelete.eventType.hosts.find((host) => {
@@ -1677,6 +1677,7 @@ describe("handleSeats", () => {
});
req.userId = organizer.id;
req.body.cancellationReason = "test cancellation reason";
await handleCancelBooking(req);
@@ -1817,6 +1818,7 @@ describe("handleSeats", () => {
});
req.userId = organizer.id;
req.body.cancellationReason = "test cancellation reason";
await handleCancelBooking(req);
@@ -2682,6 +2684,7 @@ describe("handleSeats", () => {
});
req.userId = organizer.id;
req.body.cancellationReason = "test cancellation reason";
await handleCancelBooking(req);