Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3c0cdea1 | ||
|
|
6b6d3d90e4 | ||
|
|
9b348adb6a | ||
|
|
feda420f0c | ||
|
|
4b818de0c8 | ||
|
|
614741d207 | ||
|
|
2550485c49 | ||
|
|
efc3e864bb | ||
|
|
1bf56fbe93 | ||
|
|
629629cb9e | ||
|
|
8c0751b186 | ||
|
|
0b46f61a23 | ||
|
|
0c92fbe11d |
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.4.1",
|
||||
"version": "3.4.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 { 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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
+101
-73
@@ -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) => {
|
||||
|
||||
+22
-4
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user