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:
co-authored by
Udit Takkar
Udit Takkar
Peer Richelsen
parent
1a1a506324
commit
7a9d291d1f
@@ -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}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -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"]');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user