Compare commits

...
13 Commits
Author SHA1 Message Date
Keith Williams 5e3c0cdea1 v3.4.2
Auto Comment Merge Conflicts / auto-comment-merge-conflicts (push) Canceled after 0s
release.yml / release (push) Canceled after 0s
2023-10-19 12:31:00 -03:00
sean-brydon 6b6d3d90e4 feat/use-primary-by-default-overlay (#11935) 2023-10-19 19:39:06 +05:30
9b348adb6a fix: added loading state to button (#11624)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-10-19 13:52:10 +00:00
Nafees NazikandPeer Richelsen feda420f0c fix: org team page not found with uppercase letters (#11737)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-10-19 14:44:43 +01:00
Syed Ali Shahbaz 4b818de0c8 feat: Allow hideBranding via public API (#11978) 2023-10-19 10:31:13 -03:00
614741d207 test: Create E2E tests for bookings with custom/required Phone + other questions (#11502)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2023-10-19 09:27:32 -03:00
2550485c49 feat: Shows link location and respective icon in /bookings (#11866)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-10-19 11:06:48 +02:00
Hariom BalharaandPeer Richelsen efc3e864bb fix: Missing avatar for non-migrated users on team booking page (#11977)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-10-19 08:32:56 +05:30
Joe Au-Yeung 1bf56fbe93 fix: When GCal OAuth Canceled, Do Not Create A Credential (#11987) 2023-10-18 18:35:24 -07:00
Hariom BalharaandSyed Ali Shahbaz 629629cb9e Add /embed route for booking/[uid] (#11976)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2023-10-18 18:43:12 +00:00
Peer Richelsen 8c0751b186 fix: location dropdown overflow (#11967) 2023-10-18 11:36:34 +00:00
sean-brydon 0b46f61a23 feat: (Overlay) Persist toggle option (#11961) 2023-10-18 11:16:02 +01:00
Carina WollendorferandCarinaWolli 0c92fbe11d fix: failing scheduleEmailReminder cron job (#11960)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-10-18 15:17:39 +05:30
22 changed files with 920 additions and 110 deletions
+4
View File
@@ -75,6 +75,7 @@ export const schemaUserBaseBodyParams = User.pick({
theme: true,
defaultScheduleId: true,
locale: true,
hideBranding: true,
timeFormat: true,
brandColor: true,
darkBrandColor: true,
@@ -95,6 +96,7 @@ const schemaUserEditParams = z.object({
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
@@ -115,6 +117,7 @@ const schemaUserCreateParams = z.object({
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
@@ -157,6 +160,7 @@ export const schemaUserReadPublic = User.pick({
defaultScheduleId: true,
locale: true,
timeFormat: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
+8 -4
View File
@@ -53,6 +53,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
* timeZone:
* description: The user's time zone
* type: string
* hideBranding:
* description: Remove branding from the user's calendar page
* type: boolean
* theme:
* description: Default theme for the user. Acceptable values are one of [DARK, LIGHT]
* type: string
@@ -79,7 +82,7 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
* - users
* responses:
* 200:
* description: OK, user edited successfuly
* description: OK, user edited successfully
* 400:
* description: Bad request. User body is invalid.
* 401:
@@ -94,9 +97,10 @@ export async function patchHandler(req: NextApiRequest) {
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
const body = await schemaUserEditBodyParams.parseAsync(req.body);
// disable role changes unless admin.
if (!isAdmin && body.role) {
body.role = undefined;
// disable role or branding changes unless admin.
if (!isAdmin) {
if (body.role) body.role = undefined;
if (body.hideBranding) body.hideBranding = undefined;
}
const userSchedules = await prisma.schedule.findMany({
+3
View File
@@ -42,6 +42,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
* darkBrandColor:
* description: The new user's brand color for dark mode
* type: string
* hideBranding:
* description: Remove branding from the user's calendar page
* type: boolean
* weekStart:
* description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
* type: string
@@ -88,6 +88,10 @@ function BookingListItem(booking: BookingItemProps) {
const isRecurring = booking.recurringEventId !== null;
const isTabRecurring = booking.listingStatus === "recurring";
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
const eventLocationType = getEventLocationType(booking.location);
const meetingLink = booking.references[0]?.meetingUrl
? booking.references[0]?.meetingUrl
: booking.location;
const paymentAppData = getPaymentAppData(booking.eventType);
@@ -353,6 +357,27 @@ function BookingListItem(booking: BookingItemProps) {
attendees={booking.attendees}
/>
</div>
{!isPending && (eventLocationType || booking.location?.startsWith("https://")) && (
<Link
href={meetingLink ? meetingLink.toString() : ""}
className="text-sm leading-6 text-blue-400 hover:underline">
<div className="flex items-center gap-2">
{eventLocationType ? (
<>
<img
src={eventLocationType.iconUrl}
className="h-4 w-4 rounded-sm"
alt={`${eventLocationType.label} logo`}
/>
{t("join_event_location", { eventLocationType: eventLocationType.label })}
</>
) : (
t("join_meeting")
)}
</div>
</Link>
)}
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("unconfirmed")}
@@ -425,7 +425,7 @@ function EventTypeSingleLayout({
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
<HorizontalTabs tabs={EventTypeTabs} linkShallow />
</div>
<div className="w-full overflow-hidden ltr:mr-2 rtl:ml-2">
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
className={classNames(
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.4.1",
"version": "3.4.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+24 -1
View File
@@ -34,13 +34,35 @@ async function getIdentityData(req: NextApiRequest) {
: null;
if (username) {
const user = await prisma.user.findFirst({
let user = await prisma.user.findFirst({
where: {
username,
organization: orgQuery,
},
select: { avatar: true, email: true },
});
/**
* TEMPORARY CODE STARTS - TO BE REMOVED after mono-user schema is implemented
* Try the non-org user temporarily to support users part of a team but not part of the organization
* This is needed because of a situation where we migrate a user and the team to ORG but not all the users in the team to the ORG.
* Eventually, all users will be migrated to the ORG but this is when user by user migration happens initially.
*/
// No user found in the org, try the non-org user that might be part of the team that's part of an org
if (!user && orgQuery) {
// The only side effect this code could have is that it could serve the avatar of a non-org member from the org domain but as long as the username isn't taken by an org member.
user = await prisma.user.findFirst({
where: {
username,
organization: null,
},
select: { avatar: true, email: true },
});
}
/**
* TEMPORARY CODE ENDS
*/
return {
name: username,
email: user?.email,
@@ -48,6 +70,7 @@ async function getIdentityData(req: NextApiRequest) {
org,
};
}
if (teamname) {
const team = await prisma.team.findFirst({
where: {
+12 -2
View File
@@ -1,7 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -18,6 +18,7 @@ import { ssrInit } from "@server/lib/ssr";
type Props = inferSSRProps<typeof getServerSideProps>;
export function Logout(props: Props) {
const [btnLoading, setBtnLoading] = useState<boolean>(false);
const { status } = useSession();
if (status === "authenticated") signOut({ redirect: false });
const router = useRouter();
@@ -35,6 +36,11 @@ export function Logout(props: Props) {
return "hope_to_see_you_soon";
};
const navigateToLogin = () => {
setBtnLoading(true);
router.push("/auth/login");
};
return (
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
@@ -50,7 +56,11 @@ export function Logout(props: Props) {
</div>
</div>
</div>
<Button href="/auth/login" className="flex w-full justify-center">
<Button
data-testid="logout-btn"
onClick={navigateToLogin}
className="flex w-full justify-center"
loading={btnLoading}>
{t("go_back_login")}
</Button>
</AuthContainer>
+4 -4
View File
@@ -1042,7 +1042,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true };
if (!parsedQuery.success) return { notFound: true } as const;
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
@@ -1100,7 +1100,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!bookingInfoRaw) {
return {
notFound: true,
};
} as const;
}
const eventTypeRaw = !bookingInfoRaw.eventTypeId
@@ -1109,7 +1109,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!eventTypeRaw) {
return {
notFound: true,
};
} as const;
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
@@ -1130,7 +1130,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!eventTypeRaw.owner)
return {
notFound: true,
};
} as const;
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
+7
View File
@@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[uid]";
export { default } from "../[uid]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -0,0 +1,387 @@
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With Phone Question and Each Other Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test.beforeEach(async ({ page, users, bookingPage }) => {
await loginUser(users);
await page.goto("/event-types");
await bookingPage.goToEventType("30 min");
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Booking With Phone Question and Address Question", () => {
test("Phone and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Address question (only phone required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test.describe("Booking With Phone Question and checkbox group Question", () => {
const bookingOptions = { hasPlaceholder: false, isRequired: true };
test("Phone and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox group question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox group question (only phone required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and checkbox Question", () => {
test("Phone and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and checkbox (only phone required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and Long text Question", () => {
test("Phone and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Long Text question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Long Text question (only phone required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and Multi email Question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Phone and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
true,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Email question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion(
"multiemail",
"multiemail-test",
"multiemail test",
false,
"multiemail test"
);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Email question (only phone required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and multiselect Question", () => {
test("Phone and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Select question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Multi Select question (only phone required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and Number Question", () => {
test("Phone and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Number question (only phone required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and Radio group Question", () => {
test("Phone and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Radio question (only phone required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and select Question", () => {
test("Phone and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Select question (both required)",
secondQuestion: "select",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and select not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Select question (only phone required)",
secondQuestion: "select",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
test.describe("Booking With Phone Question and Short text question", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Phone and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
test("Phone and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAndConfirmBooking({
eventTypePage,
placeholderText: "Please share anything that will help prepare for our meeting.",
question: "phone",
fillText: "Test Phone question and Text question (only phone required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.cancelAndRescheduleBooking(eventTypePage);
});
});
});
});
@@ -0,0 +1,250 @@
import { expect, type Page } from "@playwright/test";
import type { createUsersFixture } from "./users";
const reschedulePlaceholderText = "Let others know why you need to reschedule";
export const scheduleSuccessfullyText = "This meeting is scheduled";
const EMAIL = "test@test.com";
const EMAIL2 = "test2@test.com";
const PHONE = "+55 (32) 983289947";
type BookingOptions = {
hasPlaceholder?: boolean;
isReschedule?: boolean;
isRequired?: boolean;
};
interface QuestionActions {
[key: string]: () => Promise<void>;
}
type customLocators = {
shouldChangeSelectLocator: boolean;
shouldUseLastRadioGroupLocator: boolean;
shouldUseFirstRadioGroupLocator: boolean;
shouldChangeMultiSelectLocator: boolean;
};
type fillAndConfirmBookingParams = {
eventTypePage: Page;
placeholderText: string;
question: string;
fillText: string;
secondQuestion: string;
options: BookingOptions;
};
type UserFixture = ReturnType<typeof createUsersFixture>;
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
const questionActions: QuestionActions = {
phone: async () => {
await eventTypePage.locator('input[name="phone-test"]').clear();
await eventTypePage.locator('input[name="phone-test"]').fill(PHONE);
},
multiemail: async () => {
await eventTypePage.getByRole("button", { name: `${questionType} test` }).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill(EMAIL);
await eventTypePage.getByTestId("add-another-guest").last().click();
await eventTypePage.getByPlaceholder(`${questionType} test`).last().fill(EMAIL2);
},
checkbox: async () => {
if (customLocators.shouldUseLastRadioGroupLocator || customLocators.shouldChangeMultiSelectLocator) {
await eventTypePage.getByLabel("Option 1").last().click();
await eventTypePage.getByLabel("Option 2").last().click();
} else if (customLocators.shouldUseFirstRadioGroupLocator) {
await eventTypePage.getByLabel("Option 1").first().click();
await eventTypePage.getByLabel("Option 2").first().click();
} else {
await eventTypePage.getByLabel("Option 1").click();
await eventTypePage.getByLabel("Option 2").click();
}
},
multiselect: async () => {
if (customLocators.shouldChangeMultiSelectLocator) {
await eventTypePage.locator("form svg").nth(1).click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
boolean: async () => {
await eventTypePage.getByLabel(`${questionType} test`).check();
},
radio: async () => {
await eventTypePage.locator('[id="radio-test\\.option\\.0\\.radio"]').click();
},
select: async () => {
if (customLocators.shouldChangeSelectLocator) {
await eventTypePage.locator("form svg").nth(1).click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
number: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("123");
},
address: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("address test");
},
textarea: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("textarea test");
},
text: async () => {
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
},
};
if (questionActions[questionType]) {
await questionActions[questionType]();
}
};
export async function loginUser(users: UserFixture) {
const pro = await users.create({ name: "testuser" });
await pro.apiLogin();
}
export function createBookingPageFixture(page: Page) {
return {
goToEventType: async (eventType: string) => {
await page.getByRole("link", { name: eventType }).click();
},
goToTab: async (tabName: string) => {
await page.getByTestId(`vertical-tab-${tabName}`).click();
},
addQuestion: async (
questionType: string,
identifier: string,
label: string,
isRequired: boolean,
placeholder?: string
) => {
await page.getByTestId("add-field").click();
await page.locator("#test-field-type > .bg-default > div > div:nth-child(2)").first().click();
await page.getByTestId(`select-option-${questionType}`).click();
await page.getByLabel("Identifier").dblclick();
await page.getByLabel("Identifier").fill(identifier);
await page.getByLabel("Label").click();
await page.getByLabel("Label").fill(label);
if (placeholder) {
await page.getByLabel("Placeholder").click();
await page.getByLabel("Placeholder").fill(placeholder);
}
if (!isRequired) {
await page.getByRole("radio", { name: "No" }).click();
}
await page.getByTestId("field-add-save").click();
},
updateEventType: async () => {
await page.getByTestId("update-eventtype").click();
},
previewEventType: async () => {
const eventtypePromise = page.waitForEvent("popup");
await page.getByTestId("preview-button").click();
return eventtypePromise;
},
selectTimeSlot: async (eventTypePage: Page) => {
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
await eventTypePage.getByRole("button", { name: "View next" }).click();
}
await eventTypePage.getByTestId("time").first().click();
},
clickReschedule: async () => {
await page.getByText("Reschedule").click();
},
navigateToAvailableTimeSlot: async () => {
while (await page.getByRole("button", { name: "View next" }).isVisible()) {
await page.getByRole("button", { name: "View next" }).click();
}
},
selectFirstAvailableTime: async () => {
await page.getByTestId("time").first().click();
},
fillRescheduleReasonAndConfirm: async () => {
await page.getByPlaceholder(reschedulePlaceholderText).click();
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
await page.getByTestId("confirm-reschedule-button").click();
},
verifyReschedulingSuccess: async () => {
await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible();
},
cancelBookingWithReason: async () => {
await page.getByTestId("cancel").click();
await page.getByTestId("cancel_reason").fill("Test cancel");
await page.getByTestId("confirm_cancel").click();
},
verifyBookingCancellation: async () => {
await expect(page.getByTestId("cancelled-headline")).toBeVisible();
},
cancelAndRescheduleBooking: async (eventTypePage: Page) => {
await eventTypePage.getByText("Reschedule").click();
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
await eventTypePage.getByRole("button", { name: "View next" }).click();
}
await eventTypePage.getByTestId("time").first().click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
await eventTypePage.getByTestId("confirm-reschedule-button").click();
await expect(eventTypePage.getByText(scheduleSuccessfullyText)).toBeVisible();
await eventTypePage.getByTestId("cancel").click();
await eventTypePage.getByTestId("cancel_reason").fill("Test cancel");
await eventTypePage.getByTestId("confirm_cancel").click();
await expect(eventTypePage.getByTestId("cancelled-headline")).toBeVisible();
},
fillAndConfirmBooking: async ({
eventTypePage,
placeholderText,
question,
fillText,
secondQuestion,
options,
}: fillAndConfirmBookingParams) => {
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
await expect(eventTypePage.getByText(`${secondQuestion} test`).first()).toBeVisible();
await eventTypePage.getByPlaceholder(placeholderText).fill(fillText);
// Change the selector for specifics cases related to select question
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
question === "select" && ["multiemail", "multiselect"].includes(secondQuestion);
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
question === "radio" && secondQuestion === "checkbox";
const shouldUseFirstRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
question === "checkbox" && secondQuestion === "radio";
const shouldChangeMultiSelectLocator = (question: string, secondQuestion: string): boolean =>
question === "multiselect" &&
["address", "checkbox", "multiemail", "select"].includes(secondQuestion);
const customLocators = {
shouldChangeSelectLocator: shouldChangeSelectLocator(question, secondQuestion),
shouldUseLastRadioGroupLocator: shouldUseLastRadioGroupLocator(question, secondQuestion),
shouldUseFirstRadioGroupLocator: shouldUseFirstRadioGroupLocator(question, secondQuestion),
shouldChangeMultiSelectLocator: shouldChangeMultiSelectLocator(question, secondQuestion),
};
// Fill the first question
await fillQuestion(eventTypePage, question, customLocators);
// Fill the second question if is required
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
await eventTypePage.getByTestId(confirmButton).click();
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible();
},
};
}
+6
View File
@@ -10,6 +10,7 @@ import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings";
import { createEmbedsFixture } from "../fixtures/embeds";
import { createPaymentsFixture } from "../fixtures/payments";
import { createBookingPageFixture } from "../fixtures/regularBookings";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
@@ -24,6 +25,7 @@ export interface Fixtures {
prisma: typeof prisma;
emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
bookingPage: ReturnType<typeof createBookingPageFixture>;
}
declare global {
@@ -80,4 +82,8 @@ export const test = base.extend<Fixtures>({
await use(undefined);
}
},
bookingPage: async ({ page }, use) => {
const bookingPage = createBookingPageFixture(page);
await use(bookingPage);
},
});
+1 -1
View File
@@ -35,7 +35,7 @@ test.describe("user can login & logout succesfully", async () => {
const signOutBtn = await page.locator(`text=${signOutLabel}`);
await signOutBtn.click();
await page.locator('a[href="/auth/login"]').click();
await page.locator("[data-testid=logout-btn]").click();
// Reroute to the home page to check if the login form shows up
await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
@@ -2095,5 +2095,7 @@
"overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"join_event_location": "Join {{eventLocationType}}",
"join_meeting": "Join Meeting",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -16,10 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code } = req.query;
const state = decodeOAuthState(req);
if (code && typeof code !== "string") {
if (typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
@@ -39,16 +40,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (code) {
const token = await oAuth2Client.getToken(code);
key = token.res?.data;
}
await prisma.credential.create({
data: {
type: "google_calendar",
key,
userId: req.session.user.id,
appId: "google-calendar",
},
});
await prisma.credential.create({
data: {
type: "google_calendar",
key,
userId: req.session.user.id,
appId: "google-calendar",
},
});
}
if (state?.installGoogleVideo) {
const existingGoogleMeetCredential = await prisma.credential.findFirst({
@@ -19,7 +19,8 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
const schedule = useScheduleForEvent({
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
});
const displayOverlay = getQueryParam("overlayCalendar") === "true";
const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
const event = useEvent();
const eventDuration = selectedEventDuration || event?.data?.length || 30;
@@ -1,5 +1,5 @@
import { useSession } from "next-auth/react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { useState, useCallback, useEffect } from "react";
import dayjs from "@calcom/dayjs";
@@ -17,19 +17,108 @@ import { OverlayCalendarSettingsModal } from "../OverlayCalendar/OverlayCalendar
import { useLocalSet } from "../hooks/useLocalSet";
import { useOverlayCalendarStore } from "./store";
export function OverlayCalendarContainer() {
interface OverlayCalendarSwitchProps {
setContinueWithProvider: (val: boolean) => void;
setCalendarSettingsOverlay: (val: boolean) => void;
enabled?: boolean;
}
function OverlayCalendarSwitch({
setCalendarSettingsOverlay,
setContinueWithProvider,
enabled,
}: OverlayCalendarSwitchProps) {
const { t } = useLocale();
const layout = useBookerStore((state) => state.layout);
const searchParams = useSearchParams();
const { data: session } = useSession();
const router = useRouter();
const pathname = usePathname();
const switchEnabled = enabled;
// Toggle query param for overlay calendar
const toggleOverlayCalendarQueryParam = useCallback(
(state: boolean) => {
const current = new URLSearchParams(Array.from(searchParams.entries()));
if (state) {
current.set("overlayCalendar", "true");
localStorage.setItem("overlayCalendarSwitchDefault", "true");
} else {
current.delete("overlayCalendar");
localStorage.removeItem("overlayCalendarSwitchDefault");
}
// cast to string
const value = current.toString();
const query = value ? `?${value}` : "";
router.push(`${pathname}${query}`);
},
[searchParams, pathname, router]
);
/**
* If a user is not logged in and the overlay calendar query param is true,
* show the continue modal so they can login / create an account
*/
useEffect(() => {
if (!session && switchEnabled) {
toggleOverlayCalendarQueryParam(false);
setContinueWithProvider(true);
}
}, [session, switchEnabled, setContinueWithProvider, toggleOverlayCalendarQueryParam]);
return (
<div
className={classNames(
"hidden gap-2",
layout === "week_view" || layout === "column_view" ? "xl:flex" : "md:flex"
)}>
<div className="flex items-center gap-2 pr-2">
<Switch
data-testid="overlay-calendar-switch"
checked={switchEnabled}
id="overlayCalendar"
onCheckedChange={(state) => {
if (!session) {
setContinueWithProvider(state);
} else {
toggleOverlayCalendarQueryParam(state);
}
}}
/>
<label
htmlFor="overlayCalendar"
className="text-emphasis text-sm font-medium leading-none hover:cursor-pointer">
{t("overlay_my_calendar")}
</label>
</div>
{session && (
<Button
size="base"
data-testid="overlay-calendar-settings-button"
variant="icon"
color="secondary"
StartIcon={Settings}
onClick={() => {
setCalendarSettingsOverlay(true);
}}
/>
)}
</div>
);
}
export function OverlayCalendarContainer() {
const isEmbed = useIsEmbed();
const searchParams = useSearchParams();
const [continueWithProvider, setContinueWithProvider] = useState(false);
const [calendarSettingsOverlay, setCalendarSettingsOverlay] = useState(false);
const { data: session } = useSession();
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
const switchEnabled =
searchParams.get("overlayCalendar") === "true" ||
localStorage.getItem("overlayCalendarSwitchDefault") === "true";
const layout = useBookerStore((state) => state.layout);
const selectedDate = useBookerStore((state) => state.selectedDate);
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { timezone } = useTimePreferences();
// Move this to a hook
@@ -37,7 +126,6 @@ export function OverlayCalendarContainer() {
credentialId: number;
externalId: string;
}>("toggledConnectedCalendars", []);
const overlayCalendarQueryParam = searchParams.get("overlayCalendar");
const { data: overlayBusyDates } = trpc.viewer.availability.calendarOverlay.useQuery(
{
@@ -50,7 +138,7 @@ export function OverlayCalendarContainer() {
})),
},
{
enabled: !!session && set.size > 0 && overlayCalendarQueryParam === "true",
enabled: !!session && set.size > 0 && switchEnabled,
onError: () => {
clearSet();
},
@@ -76,77 +164,17 @@ export function OverlayCalendarContainer() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overlayBusyDates]);
// Toggle query param for overlay calendar
const toggleOverlayCalendarQueryParam = useCallback(
(state: boolean) => {
const current = new URLSearchParams(Array.from(searchParams.entries()));
if (state) {
current.set("overlayCalendar", "true");
} else {
current.delete("overlayCalendar");
}
// cast to string
const value = current.toString();
const query = value ? `?${value}` : "";
router.push(`${pathname}${query}`);
},
[searchParams, pathname, router]
);
/**
* If a user is not logged in and the overlay calendar query param is true,
* show the continue modal so they can login / create an account
*/
useEffect(() => {
if (!session && overlayCalendarQueryParam === "true") {
toggleOverlayCalendarQueryParam(false);
setContinueWithProvider(true);
}
}, [session, overlayCalendarQueryParam, toggleOverlayCalendarQueryParam]);
if (isEmbed) {
return null;
}
return (
<>
<div
className={classNames(
"hidden gap-2",
layout === "week_view" || layout === "column_view" ? "xl:flex" : "md:flex"
)}>
<div className="flex items-center gap-2 pr-2">
<Switch
data-testid="overlay-calendar-switch"
checked={overlayCalendarQueryParam === "true"}
id="overlayCalendar"
onCheckedChange={(state) => {
if (!session) {
setContinueWithProvider(state);
} else {
toggleOverlayCalendarQueryParam(state);
}
}}
/>
<label
htmlFor="overlayCalendar"
className="text-emphasis text-sm font-medium leading-none hover:cursor-pointer">
{t("overlay_my_calendar")}
</label>
</div>
{session && (
<Button
size="base"
data-testid="overlay-calendar-settings-button"
variant="icon"
color="secondary"
StartIcon={Settings}
onClick={() => {
setCalendarSettingsOverlay(true);
}}
/>
)}
</div>
<OverlayCalendarSwitch
setCalendarSettingsOverlay={setCalendarSettingsOverlay}
setContinueWithProvider={setContinueWithProvider}
enabled={switchEnabled}
/>
<OverlayCalendarContinueModal
open={continueWithProvider}
onClose={(val) => {
@@ -1,6 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Fragment } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -43,17 +43,35 @@ const SkeletonLoader = () => {
export function OverlayCalendarSettingsModal(props: IOverlayCalendarContinueModalProps) {
const utils = trpc.useContext();
const [initalised, setInitalised] = useState(false);
const searchParams = useSearchParams();
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery(undefined, {
enabled: !!props.open,
enabled: !!props.open || !!searchParams.get("overlayCalendar"),
});
const { toggleValue, hasItem } = useLocalSet<{
const { toggleValue, hasItem, set } = useLocalSet<{
credentialId: number;
externalId: string;
}>("toggledConnectedCalendars", []);
const router = useRouter();
const { t } = useLocale();
useEffect(() => {
if (data?.connectedCalendars && set.size === 0 && !initalised) {
data?.connectedCalendars.forEach((item) => {
item.calendars?.forEach((cal) => {
const id = { credentialId: item.credentialId, externalId: cal.externalId };
if (cal.primary) {
toggleValue(id);
}
});
});
setInitalised(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, hasItem, set, initalised]);
return (
<>
<Dialog open={props.open} onOpenChange={props.onClose}>
@@ -48,7 +48,8 @@ const SlotItem = ({
}) => {
const { t } = useLocale();
const overlayCalendarToggled = getQueryParam("overlayCalendar") === "true";
const overlayCalendarToggled =
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const bookingData = useBookerStore((state) => state.bookingData);
@@ -1,6 +1,7 @@
import type { Prisma } from "@prisma/client";
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
/**
* return the org slug
@@ -52,13 +53,14 @@ export function getOrgFullDomain(slug: string, options: { protocol: boolean } =
}
export function getSlugOrRequestedSlug(slug: string) {
const slugifiedValue = slugify(slug);
return {
OR: [
{ slug },
{ slug: slugifiedValue },
{
metadata: {
path: ["requestedSlug"],
equals: slug,
equals: slugifiedValue,
},
},
],
@@ -34,7 +34,11 @@ type Booking = Prisma.BookingGetPayload<{
};
}>;
function getiCalEventAsString(booking: Booking) {
function getiCalEventAsString(
booking: Pick<Booking, "startTime" | "endTime" | "description" | "location" | "user" | "attendees"> & {
eventType: { recurringEvent?: Prisma.JsonValue; title?: string } | null;
}
) {
let recurrenceRule: string | undefined = undefined;
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
if (recurringEvent?.count) {
@@ -114,6 +118,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
skip: pageNumber * pageSize,
take: pageSize,
select: {
referenceId: true,
},
});
if (remindersToDelete.length === 0) {
@@ -156,6 +163,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
skip: pageNumber * pageSize,
take: pageSize,
select: {
referenceId: true,
id: true,
},
});
if (remindersToCancel.length === 0) {
@@ -203,13 +214,40 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
skip: pageNumber * pageSize,
take: pageSize,
include: {
workflowStep: true,
select: {
id: true,
scheduledDate: true,
workflowStep: {
select: {
action: true,
sendTo: true,
reminderBody: true,
emailSubject: true,
template: true,
sender: true,
includeCalendarEvent: true,
},
},
booking: {
include: {
eventType: true,
select: {
startTime: true,
endTime: true,
location: true,
description: true,
user: true,
metadata: true,
uid: true,
customInputs: true,
responses: true,
attendees: true,
eventType: {
select: {
bookingFields: true,
title: true,
slug: true,
recurringEvent: true,
},
},
},
},
},