Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3c0cdea1 | ||
|
|
6b6d3d90e4 | ||
|
|
9b348adb6a | ||
|
|
feda420f0c | ||
|
|
4b818de0c8 | ||
|
|
614741d207 | ||
|
|
2550485c49 | ||
|
|
efc3e864bb | ||
|
|
1bf56fbe93 | ||
|
|
629629cb9e | ||
|
|
8c0751b186 | ||
|
|
0b46f61a23 | ||
|
|
0c92fbe11d |
No files matched your search
@@ -75,6 +75,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||||||
theme: true,
|
theme: true,
|
||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
|
hideBranding: true,
|
||||||
timeFormat: true,
|
timeFormat: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
@@ -95,6 +96,7 @@ const schemaUserEditParams = z.object({
|
|||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
|
hideBranding: z.boolean().optional(),
|
||||||
timeZone: timeZone.optional(),
|
timeZone: timeZone.optional(),
|
||||||
theme: z.nativeEnum(theme).optional().nullable(),
|
theme: z.nativeEnum(theme).optional().nullable(),
|
||||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||||
@@ -115,6 +117,7 @@ const schemaUserCreateParams = z.object({
|
|||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
|
hideBranding: z.boolean().optional(),
|
||||||
timeZone: timeZone.optional(),
|
timeZone: timeZone.optional(),
|
||||||
theme: z.nativeEnum(theme).optional().nullable(),
|
theme: z.nativeEnum(theme).optional().nullable(),
|
||||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||||
@@ -157,6 +160,7 @@ export const schemaUserReadPublic = User.pick({
|
|||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
timeFormat: true,
|
timeFormat: true,
|
||||||
|
hideBranding: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* timeZone:
|
* timeZone:
|
||||||
* description: The user's time zone
|
* description: The user's time zone
|
||||||
* type: string
|
* type: string
|
||||||
|
* hideBranding:
|
||||||
|
* description: Remove branding from the user's calendar page
|
||||||
|
* type: boolean
|
||||||
* theme:
|
* theme:
|
||||||
* description: Default theme for the user. Acceptable values are one of [DARK, LIGHT]
|
* description: Default theme for the user. Acceptable values are one of [DARK, LIGHT]
|
||||||
* type: string
|
* type: string
|
||||||
@@ -79,7 +82,7 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* - users
|
* - users
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK, user edited successfuly
|
* description: OK, user edited successfully
|
||||||
* 400:
|
* 400:
|
||||||
* description: Bad request. User body is invalid.
|
* description: Bad request. User body is invalid.
|
||||||
* 401:
|
* 401:
|
||||||
@@ -94,9 +97,10 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
||||||
// disable role changes unless admin.
|
// disable role or branding changes unless admin.
|
||||||
if (!isAdmin && body.role) {
|
if (!isAdmin) {
|
||||||
body.role = undefined;
|
if (body.role) body.role = undefined;
|
||||||
|
if (body.hideBranding) body.hideBranding = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSchedules = await prisma.schedule.findMany({
|
const userSchedules = await prisma.schedule.findMany({
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
|||||||
* darkBrandColor:
|
* darkBrandColor:
|
||||||
* description: The new user's brand color for dark mode
|
* description: The new user's brand color for dark mode
|
||||||
* type: string
|
* type: string
|
||||||
|
* hideBranding:
|
||||||
|
* description: Remove branding from the user's calendar page
|
||||||
|
* type: boolean
|
||||||
* weekStart:
|
* weekStart:
|
||||||
* description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
|
* description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
|
||||||
* type: string
|
* type: string
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const isRecurring = booking.recurringEventId !== null;
|
const isRecurring = booking.recurringEventId !== null;
|
||||||
const isTabRecurring = booking.listingStatus === "recurring";
|
const isTabRecurring = booking.listingStatus === "recurring";
|
||||||
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
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);
|
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||||
|
|
||||||
@@ -353,6 +357,27 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
attendees={booking.attendees}
|
attendees={booking.attendees}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{isPending && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||||
{t("unconfirmed")}
|
{t("unconfirmed")}
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ function EventTypeSingleLayout({
|
|||||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||||
<HorizontalTabs tabs={EventTypeTabs} linkShallow />
|
<HorizontalTabs tabs={EventTypeTabs} linkShallow />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-hidden ltr:mr-2 rtl:ml-2">
|
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
|
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -34,13 +34,35 @@ async function getIdentityData(req: NextApiRequest) {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findFirst({
|
let user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
organization: orgQuery,
|
organization: orgQuery,
|
||||||
},
|
},
|
||||||
select: { avatar: true, email: true },
|
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 {
|
return {
|
||||||
name: username,
|
name: username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
@@ -48,6 +70,7 @@ async function getIdentityData(req: NextApiRequest) {
|
|||||||
org,
|
org,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamname) {
|
if (teamname) {
|
||||||
const team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -18,6 +18,7 @@ import { ssrInit } from "@server/lib/ssr";
|
|||||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export function Logout(props: Props) {
|
export function Logout(props: Props) {
|
||||||
|
const [btnLoading, setBtnLoading] = useState<boolean>(false);
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
if (status === "authenticated") signOut({ redirect: false });
|
if (status === "authenticated") signOut({ redirect: false });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -35,6 +36,11 @@ export function Logout(props: Props) {
|
|||||||
return "hope_to_see_you_soon";
|
return "hope_to_see_you_soon";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navigateToLogin = () => {
|
||||||
|
setBtnLoading(true);
|
||||||
|
router.push("/auth/login");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -50,7 +56,11 @@ export function Logout(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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")}
|
{t("go_back_login")}
|
||||||
</Button>
|
</Button>
|
||||||
</AuthContainer>
|
</AuthContainer>
|
||||||
|
|||||||
@@ -1042,7 +1042,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
const parsedQuery = querySchema.safeParse(context.query);
|
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, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||||
|
|
||||||
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||||
@@ -1100,7 +1100,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!bookingInfoRaw) {
|
if (!bookingInfoRaw) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||||
@@ -1109,7 +1109,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!eventTypeRaw) {
|
if (!eventTypeRaw) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||||
@@ -1130,7 +1130,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!eventTypeRaw.owner)
|
if (!eventTypeRaw.owner)
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
} as const;
|
||||||
eventTypeRaw.users.push({
|
eventTypeRaw.users.push({
|
||||||
...eventTypeRaw.owner,
|
...eventTypeRaw.owner,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
|||||||
import { createBookingsFixture } from "../fixtures/bookings";
|
import { createBookingsFixture } from "../fixtures/bookings";
|
||||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||||
import { createPaymentsFixture } from "../fixtures/payments";
|
import { createPaymentsFixture } from "../fixtures/payments";
|
||||||
|
import { createBookingPageFixture } from "../fixtures/regularBookings";
|
||||||
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
||||||
import { createServersFixture } from "../fixtures/servers";
|
import { createServersFixture } from "../fixtures/servers";
|
||||||
import { createUsersFixture } from "../fixtures/users";
|
import { createUsersFixture } from "../fixtures/users";
|
||||||
@@ -24,6 +25,7 @@ export interface Fixtures {
|
|||||||
prisma: typeof prisma;
|
prisma: typeof prisma;
|
||||||
emails?: API;
|
emails?: API;
|
||||||
routingForms: ReturnType<typeof createRoutingFormsFixture>;
|
routingForms: ReturnType<typeof createRoutingFormsFixture>;
|
||||||
|
bookingPage: ReturnType<typeof createBookingPageFixture>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -80,4 +82,8 @@ export const test = base.extend<Fixtures>({
|
|||||||
await use(undefined);
|
await use(undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
bookingPage: async ({ page }, use) => {
|
||||||
|
const bookingPage = createBookingPageFixture(page);
|
||||||
|
await use(bookingPage);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -35,7 +35,7 @@ test.describe("user can login & logout succesfully", async () => {
|
|||||||
const signOutBtn = await page.locator(`text=${signOutLabel}`);
|
const signOutBtn = await page.locator(`text=${signOutLabel}`);
|
||||||
await signOutBtn.click();
|
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
|
// Reroute to the home page to check if the login form shows up
|
||||||
await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
|
await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
|
||||||
|
|||||||
@@ -2095,5 +2095,7 @@
|
|||||||
"overlay_my_calendar":"Overlay my calendar",
|
"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.",
|
"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.",
|
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"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 { code } = req.query;
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
|
|
||||||
if (code && typeof code !== "string") {
|
if (typeof code !== "string") {
|
||||||
res.status(400).json({ message: "`code` must be a string" });
|
res.status(400).json({ message: "`code` must be a string" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.session?.user?.id) {
|
if (!req.session?.user?.id) {
|
||||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
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) {
|
if (code) {
|
||||||
const token = await oAuth2Client.getToken(code);
|
const token = await oAuth2Client.getToken(code);
|
||||||
key = token.res?.data;
|
key = token.res?.data;
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.credential.create({
|
await prisma.credential.create({
|
||||||
data: {
|
data: {
|
||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
key,
|
key,
|
||||||
userId: req.session.user.id,
|
userId: req.session.user.id,
|
||||||
appId: "google-calendar",
|
appId: "google-calendar",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (state?.installGoogleVideo) {
|
if (state?.installGoogleVideo) {
|
||||||
const existingGoogleMeetCredential = await prisma.credential.findFirst({
|
const existingGoogleMeetCredential = await prisma.credential.findFirst({
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
|
|||||||
const schedule = useScheduleForEvent({
|
const schedule = useScheduleForEvent({
|
||||||
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
|
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 event = useEvent();
|
||||||
const eventDuration = selectedEventDuration || event?.data?.length || 30;
|
const eventDuration = selectedEventDuration || event?.data?.length || 30;
|
||||||
|
|||||||
+101
-73
@@ -1,5 +1,5 @@
|
|||||||
import { useSession } from "next-auth/react";
|
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 { useState, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
@@ -17,19 +17,108 @@ import { OverlayCalendarSettingsModal } from "../OverlayCalendar/OverlayCalendar
|
|||||||
import { useLocalSet } from "../hooks/useLocalSet";
|
import { useLocalSet } from "../hooks/useLocalSet";
|
||||||
import { useOverlayCalendarStore } from "./store";
|
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 { 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 isEmbed = useIsEmbed();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [continueWithProvider, setContinueWithProvider] = useState(false);
|
const [continueWithProvider, setContinueWithProvider] = useState(false);
|
||||||
const [calendarSettingsOverlay, setCalendarSettingsOverlay] = useState(false);
|
const [calendarSettingsOverlay, setCalendarSettingsOverlay] = useState(false);
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
|
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 selectedDate = useBookerStore((state) => state.selectedDate);
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const { timezone } = useTimePreferences();
|
const { timezone } = useTimePreferences();
|
||||||
|
|
||||||
// Move this to a hook
|
// Move this to a hook
|
||||||
@@ -37,7 +126,6 @@ export function OverlayCalendarContainer() {
|
|||||||
credentialId: number;
|
credentialId: number;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
}>("toggledConnectedCalendars", []);
|
}>("toggledConnectedCalendars", []);
|
||||||
const overlayCalendarQueryParam = searchParams.get("overlayCalendar");
|
|
||||||
|
|
||||||
const { data: overlayBusyDates } = trpc.viewer.availability.calendarOverlay.useQuery(
|
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: () => {
|
onError: () => {
|
||||||
clearSet();
|
clearSet();
|
||||||
},
|
},
|
||||||
@@ -76,77 +164,17 @@ export function OverlayCalendarContainer() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [overlayBusyDates]);
|
}, [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) {
|
if (isEmbed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<OverlayCalendarSwitch
|
||||||
className={classNames(
|
setCalendarSettingsOverlay={setCalendarSettingsOverlay}
|
||||||
"hidden gap-2",
|
setContinueWithProvider={setContinueWithProvider}
|
||||||
layout === "week_view" || layout === "column_view" ? "xl:flex" : "md:flex"
|
enabled={switchEnabled}
|
||||||
)}>
|
/>
|
||||||
<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>
|
|
||||||
<OverlayCalendarContinueModal
|
<OverlayCalendarContinueModal
|
||||||
open={continueWithProvider}
|
open={continueWithProvider}
|
||||||
onClose={(val) => {
|
onClose={(val) => {
|
||||||
|
|||||||
+22
-4
@@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Fragment } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -43,17 +43,35 @@ const SkeletonLoader = () => {
|
|||||||
|
|
||||||
export function OverlayCalendarSettingsModal(props: IOverlayCalendarContinueModalProps) {
|
export function OverlayCalendarSettingsModal(props: IOverlayCalendarContinueModalProps) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
const [initalised, setInitalised] = useState(false);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
|
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
|
||||||
const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
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;
|
credentialId: number;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
}>("toggledConnectedCalendars", []);
|
}>("toggledConnectedCalendars", []);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useLocale();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={props.open} onOpenChange={props.onClose}>
|
<Dialog open={props.open} onOpenChange={props.onClose}>
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ const SlotItem = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
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 [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||||
const bookingData = useBookerStore((state) => state.bookingData);
|
const bookingData = useBookerStore((state) => state.bookingData);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return the org slug
|
* return the org slug
|
||||||
@@ -52,13 +53,14 @@ export function getOrgFullDomain(slug: string, options: { protocol: boolean } =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSlugOrRequestedSlug(slug: string) {
|
export function getSlugOrRequestedSlug(slug: string) {
|
||||||
|
const slugifiedValue = slugify(slug);
|
||||||
return {
|
return {
|
||||||
OR: [
|
OR: [
|
||||||
{ slug },
|
{ slug: slugifiedValue },
|
||||||
{
|
{
|
||||||
metadata: {
|
metadata: {
|
||||||
path: ["requestedSlug"],
|
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;
|
let recurrenceRule: string | undefined = undefined;
|
||||||
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
||||||
if (recurringEvent?.count) {
|
if (recurringEvent?.count) {
|
||||||
@@ -114,6 +118,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
skip: pageNumber * pageSize,
|
skip: pageNumber * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
select: {
|
||||||
|
referenceId: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remindersToDelete.length === 0) {
|
if (remindersToDelete.length === 0) {
|
||||||
@@ -156,6 +163,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
skip: pageNumber * pageSize,
|
skip: pageNumber * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
select: {
|
||||||
|
referenceId: true,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remindersToCancel.length === 0) {
|
if (remindersToCancel.length === 0) {
|
||||||
@@ -203,13 +214,40 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
skip: pageNumber * pageSize,
|
skip: pageNumber * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
include: {
|
select: {
|
||||||
workflowStep: true,
|
id: true,
|
||||||
|
scheduledDate: true,
|
||||||
|
workflowStep: {
|
||||||
|
select: {
|
||||||
|
action: true,
|
||||||
|
sendTo: true,
|
||||||
|
reminderBody: true,
|
||||||
|
emailSubject: true,
|
||||||
|
template: true,
|
||||||
|
sender: true,
|
||||||
|
includeCalendarEvent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
booking: {
|
booking: {
|
||||||
include: {
|
select: {
|
||||||
eventType: true,
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
location: true,
|
||||||
|
description: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
metadata: true,
|
||||||
|
uid: true,
|
||||||
|
customInputs: true,
|
||||||
|
responses: true,
|
||||||
attendees: true,
|
attendees: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
bookingFields: true,
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user