Compare commits

..

18 Commits

Author SHA1 Message Date
gitstart-calcom ebb9b9f459 FIx failing tests 2023-10-30 19:51:09 +00:00
GitStart-Cal.com 032bd9d373
Merge branch 'main' into teste2e-allQuestions 2023-10-31 01:19:37 +05:45
GitStart-Cal.com d45ccc15ba
Merge branch 'main' into teste2e-allQuestions 2023-10-30 20:51:37 +05:45
GitStart-Cal.com 2b2f1abbec
Merge branch 'main' into teste2e-allQuestions 2023-10-26 13:42:51 +00:00
gitstart-calcom 18facb7626 refactor 2023-10-24 22:05:43 +00:00
gitstart-calcom 65b614182d Merge commit '79c1aa60a2676ce5d1d96df1c98a7c6a30932cde' into teste2e-allQuestions 2023-10-24 22:05:09 +00:00
gitstart-calcom d6b7620b84 Requested changes 2023-10-24 14:31:14 +00:00
gitstart-calcom 9a17ffbdc3 Merge commit '9250b91bb0d5a66ccf2cf42311ac9999c79f6a84' into teste2e-allQuestions 2023-10-24 14:30:33 +00:00
gitstart-calcom 398e00afed update branch 2023-10-20 19:15:06 +00:00
GitStart-Cal.com 19fe881094
Update regularBookings.ts 2023-10-20 16:08:23 -03:00
GitStart-Cal.com 5dd1a0f7a4
Merge branch 'main' into teste2e-allQuestions 2023-10-20 19:07:12 +00:00
gitstart-calcom 61ead4f4a2 Requested changes 2023-10-10 21:49:20 +00:00
gitstart-calcom 3e2d0ce603 Merge commit '0f92afb4bd65a7b59010803a8a6528b5ec9bd5a5' into teste2e-allQuestions 2023-10-10 21:48:52 +00:00
GitStart-Cal.com a48ed7e126
Merge branch 'main' into teste2e-allQuestions 2023-10-06 21:33:13 +00:00
gitstart-calcom 5f32d4652a Remove unnecessary changes 2023-10-06 16:27:58 +00:00
gitstart-calcom 972cb6b9af Merge commit 'a90848edcbb9392957a600093fb76be22b1169c9' into teste2e-allQuestions 2023-10-06 16:27:27 +00:00
GitStart-Cal.com 06612d3dfb
Merge branch 'main' into teste2e-allQuestions 2023-09-28 22:26:16 +00:00
gitstart-calcom 2d154979d2 Add E2E tests for all questions in a regular booking 2023-09-28 18:36:24 +00:00
22 changed files with 277 additions and 858 deletions

18
.github/workflows/merge-conflict.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Auto Comment Merge Conflicts
on: push
permissions:
pull-requests: write
jobs:
auto-comment-merge-conflicts:
runs-on: ubuntu-latest
steps:
- uses: codytseng/auto-comment-merge-conflicts@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
comment-body: "Hey there, there is a merge conflict, can you take a look?"
wait-ms: 3000
max-retries: 5
label-name: "🚨 merge conflict"
ignore-authors: dependabot,otherAuthor

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod"; import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({ export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
credentialId: true,
integration: true, integration: true,
externalId: true, externalId: true,
eventTypeId: true, eventTypeId: true,
@ -14,6 +15,7 @@ const schemaDestinationCalendarCreateParams = z
.object({ .object({
integration: z.string(), integration: z.string(),
externalId: z.string(), externalId: z.string(),
credentialId: z.number(),
eventTypeId: z.number().optional(), eventTypeId: z.number().optional(),
bookingId: z.number().optional(), bookingId: z.number().optional(),
userId: z.number().optional(), userId: z.number().optional(),
@ -45,4 +47,5 @@ export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
eventTypeId: true, eventTypeId: true,
bookingId: true, bookingId: true,
userId: true, userId: true,
credentialId: true,
}); });

View File

@ -1,12 +1,6 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import type { z } from "zod";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server"; import { defaultResponder } from "@calcom/lib/server";
import type { PrismaClient } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { import {
schemaDestinationCalendarEditBodyParams, schemaDestinationCalendarEditBodyParams,
@ -62,251 +56,16 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* 404: * 404:
* description: Destination calendar not found * description: Destination calendar not found
*/ */
type DestinationCalendarType = {
userId?: number | null;
eventTypeId?: number | null;
credentialId: number | null;
};
type UserCredentialType = {
id: number;
appId: string | null;
type: string;
userId: number | null;
user: {
email: string;
} | null;
teamId: number | null;
key: Prisma.JsonValue;
invalid: boolean | null;
};
export async function patchHandler(req: NextApiRequest) { export async function patchHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma, query, body } = req; const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query); const { id } = schemaQueryIdParseInt.parse(query);
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body); const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
validateIntegrationInput(parsedBody);
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
const userCredentials = await getUserCredentials({
credentialId: destinationCalendarObject.credentialId,
userId: assignedUserId,
prisma,
});
const credentialId = await verifyCredentialsAndGetId({
parsedBody,
userCredentials,
currentCredentialId: destinationCalendarObject.credentialId,
});
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
const destinationCalendar = await prisma.destinationCalendar.update({ const destinationCalendar = await prisma.destinationCalendar.update({
where: { id }, where: { id },
data: { ...parsedBody, credentialId }, data: parsedBody,
}); });
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) }; return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
} }
/**
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
*
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
* @param userId - The user ID against which the credentials need to be verified.
* @param prisma - An instance of PrismaClient for database operations.
*
* @returns - An array containing the matching user credentials.
*
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
*/
async function getUserCredentials({
credentialId,
userId,
prisma,
}: {
credentialId: number | null;
userId: number;
prisma: PrismaClient;
}) {
if (!credentialId) {
throw new HttpError({
statusCode: 404,
message: `Destination calendar missing credential id`,
});
}
const userCredentials = await prisma.credential.findMany({
where: { id: credentialId, userId },
select: credentialForCalendarServiceSelect,
});
if (!userCredentials || userCredentials.length === 0) {
throw new HttpError({
statusCode: 400,
message: `Bad request, no associated credentials found`,
});
}
return userCredentials;
}
/**
* Verifies the provided credentials and retrieves the associated credential ID.
*
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
* and checks for a matching external ID and integration from the list of connected calendars.
*
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
*
* If the parsed body does not contain the necessary properties, the function
* returns the `credentialId` from the destination calendar object.
*
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
* Checked if it contain properties like `integration` and `externalId`.
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
* to fetch the default `credentialId`.
*
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
* or the provided destination calendar object in other cases.
*
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
*/
async function verifyCredentialsAndGetId({
parsedBody,
userCredentials,
currentCredentialId,
}: {
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
userCredentials: UserCredentialType[];
currentCredentialId: number | null;
}) {
if (parsedBody.integration && parsedBody.externalId) {
const calendarCredentials = getCalendarCredentials(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
[],
parsedBody.externalId
);
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
const calendar = eligibleCalendars?.find(
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
);
if (!calendar?.credentialId)
throw new HttpError({
statusCode: 400,
message: "Bad request, credential id invalid",
});
return calendar?.credentialId;
}
return currentCredentialId;
}
/**
* Validates the request for updating a destination calendar.
*
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
*
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
*
* @param destinationCalendarObject - An object representing the destination calendar.
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
* @param prisma - An instance of PrismaClient for database operations.
*
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
*/
async function validateRequestAndOwnership({
destinationCalendarObject,
parsedBody,
assignedUserId,
prisma,
}: {
destinationCalendarObject: DestinationCalendarType;
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
assignedUserId: number;
prisma: PrismaClient;
}) {
if (parsedBody.eventTypeId) {
if (!destinationCalendarObject.eventTypeId) {
throw new HttpError({
statusCode: 400,
message: `The provided destination calendar can not be linked to an event type`,
});
}
const userEventType = await prisma.eventType.findFirst({
where: { id: parsedBody.eventTypeId },
select: { userId: true },
});
if (!userEventType || userEventType.userId !== assignedUserId) {
throw new HttpError({
statusCode: 404,
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
});
}
}
if (!parsedBody.eventTypeId) {
if (destinationCalendarObject.eventTypeId) {
throw new HttpError({
statusCode: 400,
message: `The provided destination calendar can only be linked to an event type`,
});
}
if (destinationCalendarObject.userId !== assignedUserId) {
throw new HttpError({
statusCode: 403,
message: `Forbidden`,
});
}
}
}
/**
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
*
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
* indicating that the desired destination calendar was not found is thrown.
*
* @param id - The ID of the destination calendar to be retrieved.
* @param prisma - An instance of PrismaClient for database operations.
*
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
*
* @throws HttpError - If no destination calendar matches the provided ID.
*/
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
where: {
id,
},
select: { userId: true, eventTypeId: true, credentialId: true },
});
if (!destinationCalendarObject) {
throw new HttpError({
statusCode: 404,
message: `Destination calendar with ID ${id} not found`,
});
}
return destinationCalendarObject;
}
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
if (parsedBody.integration && !parsedBody.externalId) {
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
}
if (!parsedBody.integration && parsedBody.externalId) {
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
}
}
export default defaultResponder(patchHandler); export default defaultResponder(patchHandler);

View File

@ -1,9 +1,7 @@
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server"; import { defaultResponder } from "@calcom/lib/server";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { import {
schemaDestinationCalendarReadPublic, schemaDestinationCalendarReadPublic,
@ -40,6 +38,9 @@ import {
* externalId: * externalId:
* type: string * type: string
* description: 'The external ID of the integration' * description: 'The external ID of the integration'
* credentialId:
* type: integer
* description: 'The credential ID it is associated with'
* eventTypeId: * eventTypeId:
* type: integer * type: integer
* description: 'The ID of the eventType it is associated with' * description: 'The ID of the eventType it is associated with'
@ -64,38 +65,20 @@ async function postHandler(req: NextApiRequest) {
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body); const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
await checkPermissions(req, userId); await checkPermissions(req, userId);
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId; const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
/* Check if credentialId data matches the ownership and integration passed in */ /* Check if credentialId data matches the ownership and integration passed in */
const userCredentials = await prisma.credential.findMany({ const credential = await prisma.credential.findFirst({
where: { where: { type: parsedBody.integration, userId: assignedUserId },
type: parsedBody.integration, select: { id: true, type: true, userId: true },
userId: assignedUserId,
},
select: credentialForCalendarServiceSelect,
}); });
if (userCredentials.length === 0) if (!credential)
throw new HttpError({ throw new HttpError({
statusCode: 400, statusCode: 400,
message: "Bad request, credential id invalid", message: "Bad request, credential id invalid",
}); });
const calendarCredentials = getCalendarCredentials(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
const calendar = eligibleCalendars?.find(
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
);
if (!calendar?.credentialId)
throw new HttpError({
statusCode: 400,
message: "Bad request, credential id invalid",
});
const credentialId = calendar.credentialId;
if (parsedBody.eventTypeId) { if (parsedBody.eventTypeId) {
const eventType = await prisma.eventType.findFirst({ const eventType = await prisma.eventType.findFirst({
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId }, where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
@ -108,9 +91,7 @@ async function postHandler(req: NextApiRequest) {
parsedBody.userId = undefined; parsedBody.userId = undefined;
} }
const destination_calendar = await prisma.destinationCalendar.create({ const destination_calendar = await prisma.destinationCalendar.create({ data: { ...parsedBody } });
data: { ...parsedBody, credentialId },
});
return { return {
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar), destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),

View File

@ -141,6 +141,17 @@ function BookingListItem(booking: BookingItemProps) {
: []), : []),
]; ];
const showRecordingActions: ActionType[] = [
{
id: "view_recordings",
label: t("view_recordings"),
onClick: () => {
setViewRecordingsDialogIsOpen(true);
},
disabled: mutation.isLoading,
},
];
let bookedActions: ActionType[] = [ let bookedActions: ActionType[] = [
{ {
id: "cancel", id: "cancel",
@ -259,21 +270,11 @@ function BookingListItem(booking: BookingItemProps) {
const bookingLink = buildBookingLink(); const bookingLink = buildBookingLink();
const title = booking.title; const title = booking.title;
// To be used after we run query on legacy bookings
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed); const showRecordingsButtons =
const checkForRecordingsButton = (booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
!showRecordingsButtons && (booking.location === "integrations:daily" || booking?.location?.trim() === "");
const showRecordingActions: ActionType[] = [
{
id: checkForRecordingsButton ? "check_for_recordings" : "view_recordings",
label: checkForRecordingsButton ? t("check_for_recordings") : t("view_recordings"),
onClick: () => {
setViewRecordingsDialogIsOpen(true);
},
disabled: mutation.isLoading,
},
];
return ( return (
<> <>
@ -298,7 +299,7 @@ function BookingListItem(booking: BookingItemProps) {
paymentCurrency={booking.payment[0].currency} paymentCurrency={booking.payment[0].currency}
/> />
)} )}
{(showRecordingsButtons || checkForRecordingsButton) && ( {showRecordingsButtons && (
<ViewRecordingsDialog <ViewRecordingsDialog
booking={booking} booking={booking}
isOpenDialog={viewRecordingsDialogIsOpen} isOpenDialog={viewRecordingsDialogIsOpen}
@ -468,9 +469,7 @@ function BookingListItem(booking: BookingItemProps) {
</> </>
) : null} ) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null} {isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{(showRecordingsButtons || checkForRecordingsButton) && ( {showRecordingsButtons && <TableActions actions={showRecordingActions} />}
<TableActions actions={showRecordingActions} />
)}
{isCancelled && booking.rescheduled && ( {isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex"> <div className="hidden h-full items-center md:flex">
<RequestSentMessage /> <RequestSentMessage />

View File

@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "3.4.6", "version": "3.4.5",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",

View File

@ -62,46 +62,6 @@ const triggerWebhook = async ({
await Promise.all(promises); await Promise.all(promises);
}; };
const checkIfUserIsPartOfTheSameTeam = async (
teamId: number | undefined | null,
userId: number,
userEmail: string | undefined | null
) => {
if (!teamId) return false;
const getUserQuery = () => {
if (!!userEmail) {
return {
OR: [
{
id: userId,
},
{
email: userEmail,
},
],
};
} else {
return {
id: userId,
};
}
};
const team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
user: getUserQuery(),
},
},
},
});
return !!team;
};
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) { if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" }); return res.status(405).json({ message: "No SendGrid API key or email" });
@ -177,22 +137,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const isUserAttendeeOrOrganiser = const isUserAttendeeOrOrganiser =
booking?.user?.id === session.user.id || booking?.user?.id === session.user.id ||
attendeesList.find( attendeesList.find((attendee) => attendee.id === session.user.id);
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
);
if (!isUserAttendeeOrOrganiser) { if (!isUserAttendeeOrOrganiser) {
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam( return res.status(403).send({
booking?.eventType?.teamId, message: "Unauthorised",
session.user.id, });
session.user.email
);
if (!isUserMemberOfTheTeam) {
return res.status(403).send({
message: "Unauthorised",
});
}
} }
await prisma.booking.update({ await prisma.booking.update({
@ -252,7 +202,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(403).json({ message: "User does not have team plan to send out emails" }); return res.status(403).json({ message: "User does not have team plan to send out emails" });
} catch (err) { } catch (err) {
console.warn("Error in /recorded-daily-video", err); console.warn("something_went_wrong", err);
return res.status(500).json({ message: "something went wrong" }); return res.status(500).json({ message: "something went wrong" });
} }
} }

View File

@ -342,17 +342,14 @@ export default function Success(props: SuccessProps) {
<div <div
className={classNames( className={classNames(
shouldAlignCentrally ? "text-center" : "", shouldAlignCentrally ? "text-center" : "",
"flex items-end justify-center px-4 pb-20 pt-4 sm:flex sm:p-0" "flex items-end justify-center px-4 pb-20 pt-4 sm:block sm:p-0"
)}> )}>
<div <div
className={classNames( className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
"main my-4 flex flex-col transition-opacity sm:my-0 ",
isEmbed ? "" : " inset-0"
)}
aria-hidden="true"> aria-hidden="true">
<div <div
className={classNames( className={classNames(
"inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl", "main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width", !isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle" "px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
)} )}

View File

@ -164,13 +164,14 @@ const PasswordView = ({ user }: PasswordViewProps) => {
<> <>
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} /> <Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
{user && user.identityProvider !== IdentityProvider.CAL ? ( {user && user.identityProvider !== IdentityProvider.CAL ? (
<div className="border-subtle rounded-b-xl border border-t-0 px-4 py-6 sm:px-6"> <div>
<h2 className="font-cal text-emphasis text-lg font-medium leading-6"> <div className="mt-6">
{t("account_managed_by_identity_provider", { <h2 className="font-cal text-emphasis text-lg font-medium leading-6">
provider: identityProviderNameMap[user.identityProvider], {t("account_managed_by_identity_provider", {
})} provider: identityProviderNameMap[user.identityProvider],
</h2> })}
</h2>
</div>
<p className="text-subtle mt-1 text-sm"> <p className="text-subtle mt-1 text-sm">
{t("account_managed_by_identity_provider_description", { {t("account_managed_by_identity_provider_description", {
provider: identityProviderNameMap[user.identityProvider], provider: identityProviderNameMap[user.identityProvider],
@ -179,7 +180,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
</div> </div>
) : ( ) : (
<Form form={formMethods} handleSubmit={handleSubmit}> <Form form={formMethods} handleSubmit={handleSubmit}>
<div className="border-subtle border-x px-4 py-6 sm:px-6"> <div className="border-x px-4 py-6 sm:px-6">
{formMethods.formState.errors.apiError && ( {formMethods.formState.errors.apiError && (
<div className="pb-6"> <div className="pb-6">
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} /> <Alert severity="error" message={formMethods.formState.errors.apiError?.message} />

View File

@ -0,0 +1,110 @@
/* eslint-disable playwright/no-conditional-in-test */
import { loginUser } from "../fixtures/regularBookings";
import { test } from "../lib/fixtures";
test.describe("Booking With All Questions", () => {
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");
});
const bookingOptions = { isAllRequired: true };
test("Selecting and filling all questions as required", async ({ bookingPage }) => {
const allQuestions = [
"phone",
"address",
"checkbox",
"boolean",
"textarea",
"multiemail",
"multiselect",
"number",
"radio",
"select",
"text",
];
for (const question of allQuestions) {
if (
question !== "number" &&
question !== "select" &&
question !== "checkbox" &&
question !== "boolean" &&
question !== "multiselect" &&
question !== "radio"
) {
await bookingPage.addQuestion(
question,
`${question}-test`,
`${question} test`,
true,
`${question} test`
);
} else {
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
}
await bookingPage.checkField(question);
}
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, bookingOptions);
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Selecting and filling all questions as optional", async ({ bookingPage }) => {
const allQuestions = [
"phone",
"address",
"checkbox",
"boolean",
"textarea",
"multiemail",
"multiselect",
"number",
"radio",
"select",
"text",
];
for (const question of allQuestions) {
if (
question !== "number" &&
question !== "select" &&
question !== "checkbox" &&
question !== "boolean" &&
question !== "multiselect" &&
question !== "radio"
) {
await bookingPage.addQuestion(
question,
`${question}-test`,
`${question} test`,
false,
`${question} test`
);
} else {
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, false);
}
await bookingPage.checkField(question);
}
await bookingPage.updateEventType();
const eventTypePage = await bookingPage.previewEventType();
await bookingPage.selectTimeSlot(eventTypePage);
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, {
...bookingOptions,
isAllRequired: false,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});

View File

@ -1,441 +0,0 @@
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 Select Question and Address Question", () => {
test("Select required and Address required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and Address question (both required)",
secondQuestion: "address",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Address not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and Address question (only select required)",
secondQuestion: "address",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and checkbox group Question", () => {
test("Select required and checkbox group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and Checkbox question (both required)",
secondQuestion: "checkbox",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and checkbox group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and Checkbox question (only Select required)",
secondQuestion: "checkbox",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and checkbox Question", () => {
test("Select required and checkbox required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and boolean question (both required)",
secondQuestion: "boolean",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and checkbox not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and boolean question (only select required)",
secondQuestion: "boolean",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Long text Question", () => {
test("Select required and Long text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and textarea question (both required)",
secondQuestion: "textarea",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Long text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and textarea question (only select required)",
secondQuestion: "textarea",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Multi email Question", () => {
test("Select required and Multi email required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and multiemail question (both required)",
secondQuestion: "multiemail",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Multi email not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and multiemail question (only select required)",
secondQuestion: "multiemail",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and multiselect Question", () => {
test("Select required and multiselect text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and multiselect question (both required)",
secondQuestion: "multiselect",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and multiselect text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and multiselect question (only select required)",
secondQuestion: "multiselect",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Number Question", () => {
test("Select required and Number required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and number question (both required)",
secondQuestion: "number",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Number not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and number question (only select required)",
secondQuestion: "number",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Phone Question", () => {
test("Select required and select required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone 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: "select",
fillText: "Test Select question and phone question (both required)",
secondQuestion: "phone",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Phone not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone 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: "select",
fillText: "Test Select question and phone question (only select required)",
secondQuestion: "phone",
options: { ...bookingOptions, isRequired: false },
});
});
});
test.describe("Booking With Select Question and Radio group Question", () => {
test("Select required and Radio group required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and radio question (both required)",
secondQuestion: "radio",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Radio group not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and radio question (only select required)",
secondQuestion: "radio",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
test.describe("Booking With Select Question and Short text question", () => {
test("Select required and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and text question (both required)",
secondQuestion: "text",
options: bookingOptions,
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
test("Select and Short text not required", async ({ bookingPage }) => {
await bookingPage.addQuestion("select", "select-test", "select test", true, "select 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: "select",
fillText: "Test Select question and text question (only select required)",
secondQuestion: "text",
options: { ...bookingOptions, isRequired: false },
});
await bookingPage.rescheduleBooking(eventTypePage);
await bookingPage.assertBookingRescheduled(eventTypePage);
await bookingPage.cancelBooking(eventTypePage);
await bookingPage.assertBookingCanceled(eventTypePage);
});
});
});

View File

@ -1,7 +1,5 @@
import { expect, type Page } from "@playwright/test"; import { expect, type Page } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import type { createUsersFixture } from "./users"; import type { createUsersFixture } from "./users";
const reschedulePlaceholderText = "Let others know why you need to reschedule"; const reschedulePlaceholderText = "Let others know why you need to reschedule";
@ -15,6 +13,7 @@ type BookingOptions = {
hasPlaceholder?: boolean; hasPlaceholder?: boolean;
isReschedule?: boolean; isReschedule?: boolean;
isRequired?: boolean; isRequired?: boolean;
isAllRequired?: boolean;
isMultiSelect?: boolean; isMultiSelect?: boolean;
}; };
@ -40,12 +39,6 @@ type fillAndConfirmBookingParams = {
type UserFixture = ReturnType<typeof createUsersFixture>; type UserFixture = ReturnType<typeof createUsersFixture>;
function isLastDayOfMonth(): boolean {
const today = dayjs();
const endOfMonth = today.endOf("month");
return today.isSame(endOfMonth, "day");
}
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => { const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
const questionActions: QuestionActions = { const questionActions: QuestionActions = {
phone: async () => { phone: async () => {
@ -111,27 +104,72 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test"); await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
}, },
}; };
if (questionActions[questionType]) { if (questionActions[questionType]) {
await questionActions[questionType](); await questionActions[questionType]();
} }
}; };
const fillAllQuestions = async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
if (options.isAllRequired) {
for (const question of questions) {
switch (question) {
case "email":
await eventTypePage.getByPlaceholder("Email").click();
await eventTypePage.getByPlaceholder("Email").fill(EMAIL);
break;
case "phone":
await eventTypePage.getByPlaceholder("Phone test").click();
await eventTypePage.getByPlaceholder("Phone test").fill(PHONE);
break;
case "address":
await eventTypePage.getByPlaceholder("Address test").click();
await eventTypePage.getByPlaceholder("Address test").fill("123 Main St, City, Country");
break;
case "textarea":
await eventTypePage.getByPlaceholder("Textarea test").click();
await eventTypePage.getByPlaceholder("Textarea test").fill("This is a sample text for textarea.");
break;
case "select":
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "multiselect":
await eventTypePage.locator("form svg").nth(4).click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "number":
await eventTypePage.getByLabel("number test").click();
await eventTypePage.getByLabel("number test").fill("123");
break;
case "radio":
await eventTypePage.getByRole("radiogroup").getByText("Option 1").check();
break;
case "text":
await eventTypePage.getByPlaceholder("Text test").click();
await eventTypePage.getByPlaceholder("Text test").fill("Sample text");
break;
case "checkbox":
await eventTypePage.getByLabel("Option 1").first().check();
await eventTypePage.getByLabel("Option 2").first().check();
break;
case "boolean":
await eventTypePage.getByLabel(`${question} test`).check();
break;
case "multiemail":
await eventTypePage.getByRole("button", { name: "multiemail test" }).click();
await eventTypePage.getByPlaceholder("multiemail test").fill(EMAIL);
break;
}
}
}
};
export async function loginUser(users: UserFixture) { export async function loginUser(users: UserFixture) {
const pro = await users.create({ name: "testuser" }); const pro = await users.create({ name: "testuser" });
await pro.apiLogin(); await pro.apiLogin();
} }
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
try {
if (isLastDayOfMonth()) {
await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 });
await eventTypePage.getByTestId("view_next_month").click();
}
} catch (err) {
console.info("No need to click on view next month button");
}
};
export function createBookingPageFixture(page: Page) { export function createBookingPageFixture(page: Page) {
return { return {
goToEventType: async (eventType: string) => { goToEventType: async (eventType: string) => {
@ -172,13 +210,19 @@ export function createBookingPageFixture(page: Page) {
return eventtypePromise; return eventtypePromise;
}, },
selectTimeSlot: async (eventTypePage: Page) => { selectTimeSlot: async (eventTypePage: Page) => {
await goToNextMonthIfNoAvailabilities(eventTypePage); 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.getByTestId("time").first().click();
}, },
clickReschedule: async () => { clickReschedule: async () => {
await page.getByText("Reschedule").click(); 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 () => { selectFirstAvailableTime: async () => {
await page.getByTestId("time").first().click(); await page.getByTestId("time").first().click();
}, },
@ -198,7 +242,6 @@ export function createBookingPageFixture(page: Page) {
}, },
rescheduleBooking: async (eventTypePage: Page) => { rescheduleBooking: async (eventTypePage: Page) => {
await goToNextMonthIfNoAvailabilities(eventTypePage);
await eventTypePage.getByText("Reschedule").click(); await eventTypePage.getByText("Reschedule").click();
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) { while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
await eventTypePage.getByRole("button", { name: "View next" }).click(); await eventTypePage.getByRole("button", { name: "View next" }).click();
@ -207,6 +250,14 @@ export function createBookingPageFixture(page: Page) {
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click(); await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule"); await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
await eventTypePage.getByTestId("confirm-reschedule-button").click(); await eventTypePage.getByTestId("confirm-reschedule-button").click();
await eventTypePage.waitForTimeout(400);
if (
await eventTypePage.getByRole("heading", { name: "Could not reschedule the meeting." }).isVisible()
) {
await eventTypePage.getByTestId("back").click();
await eventTypePage.getByTestId("time").last().click();
await eventTypePage.getByTestId("confirm-reschedule-button").click();
}
}, },
assertBookingRescheduled: async (page: Page) => { assertBookingRescheduled: async (page: Page) => {
@ -235,7 +286,7 @@ export function createBookingPageFixture(page: Page) {
// Change the selector for specifics cases related to select question // Change the selector for specifics cases related to select question
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean => const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion); question === "select" && ["multiemail", "multiselect"].includes(secondQuestion);
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean => const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
question === "radio" && secondQuestion === "checkbox"; question === "radio" && secondQuestion === "checkbox";
@ -265,5 +316,23 @@ export function createBookingPageFixture(page: Page) {
await scheduleSuccessfullyPage.waitFor({ state: "visible" }); await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible(); await expect(scheduleSuccessfullyPage).toBeVisible();
}, },
checkField: async (question: string) => {
await expect(page.getByTestId(`field-${question}-test`)).toBeVisible();
},
fillAllQuestions: async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
await fillAllQuestions(eventTypePage, questions, options);
await eventTypePage.getByTestId(confirmButton).click();
await eventTypePage.waitForTimeout(400);
if (await eventTypePage.getByRole("heading", { name: "Could not book the meeting." }).isVisible()) {
await eventTypePage.getByTestId("back").click();
await eventTypePage.getByTestId("time").last().click();
await fillAllQuestions(eventTypePage, questions, options);
await eventTypePage.getByTestId(confirmButton).click();
}
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible();
},
}; };
} }

View File

@ -1296,7 +1296,6 @@
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.", "select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
"check_for_conflicts": "Check for conflicts", "check_for_conflicts": "Check for conflicts",
"view_recordings": "View recordings", "view_recordings": "View recordings",
"check_for_recordings":"Check for recordings",
"adding_events_to": "Adding events to", "adding_events_to": "Adding events to",
"follow_system_preferences": "Follow system preferences", "follow_system_preferences": "Follow system preferences",
"custom_brand_colors": "Custom brand colors", "custom_brand_colors": "Custom brand colors",

View File

@ -161,8 +161,8 @@ const EmailStep = (props: { translationString: string; iconsrc: string }) => {
style={{ style={{
backgroundColor: "#E5E7EB", backgroundColor: "#E5E7EB",
borderRadius: "48px", borderRadius: "48px",
minHeight: "48px", height: "48px",
minWidth: "48px", width: "48px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",

View File

@ -50,13 +50,8 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format }); return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format });
} }
protected getLocale(): string {
return this.calEvent.organizer.language.locale;
}
protected getFormattedDate() { protected getFormattedDate() {
const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR; const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd( return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd(
organizerTimeFormat organizerTimeFormat
)}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t( )}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(

View File

@ -95,7 +95,7 @@ const NoAvailabilityOverlay = ({
return ( return (
<div className="bg-muted border-subtle absolute left-1/2 top-40 -mt-10 w-max -translate-x-1/2 -translate-y-1/2 transform rounded-md border p-8 shadow-sm"> <div className="bg-muted border-subtle absolute left-1/2 top-40 -mt-10 w-max -translate-x-1/2 -translate-y-1/2 transform rounded-md border p-8 shadow-sm">
<h4 className="text-emphasis mb-4 font-medium">{t("no_availability_in_month", { month: month })}</h4> <h4 className="text-emphasis mb-4 font-medium">{t("no_availability_in_month", { month: month })}</h4>
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight} data-testid="view_next_month"> <Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight}>
{t("view_next_month")} {t("view_next_month")}
</Button> </Button>
</div> </div>

View File

@ -1,6 +1,5 @@
import { describe, expect, test, vi } from "vitest"; import { describe, expect, test, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
@ -64,22 +63,5 @@ describe("Test Suite: Date Picker", () => {
vi.setSystemTime(vi.getRealSystemTime()); vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers(); vi.useRealTimers();
}); });
test("it returns the correct responses end of month", () => {
// test a date at one minute past midnight, end of month.
// we use dayjs() as the system timezone can still modify the Date.
vi.useFakeTimers().setSystemTime(dayjs().endOf("month").startOf("day").add(1, "second").toDate());
const currentDate = new Date();
const result = getAvailableDatesInMonth({
browsingDate: currentDate,
});
expect(result).toHaveLength(1);
// Undo the forced time we applied earlier, reset to system default.
vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers();
});
}); });
}); });

View File

@ -1,4 +1,3 @@
import dayjs from "@calcom/dayjs";
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
// calculate the available dates in the month: // calculate the available dates in the month:
@ -22,9 +21,7 @@ export function getAvailableDatesInMonth({
); );
for ( for (
let date = browsingDate > minDate ? browsingDate : minDate; let date = browsingDate > minDate ? browsingDate : minDate;
// Check if date is before the last date of the month date <= lastDateOfMonth;
// or is the same day, in the same month, in the same year.
date < lastDateOfMonth || dayjs(date).isSame(lastDateOfMonth, "day");
date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
) { ) {
// intersect included dates // intersect included dates

View File

@ -194,7 +194,7 @@ const OrgAppearanceView = ({
/> />
</div> </div>
) : ( ) : (
<div className="py-5"> <div className="border-subtle rounded-md border p-5">
<span className="text-default text-sm">{t("only_owner_change")}</span> <span className="text-default text-sm">{t("only_owner_change")}</span>
</div> </div>
)} )}

View File

@ -113,7 +113,7 @@ const OrgProfileView = () => {
{isOrgAdminOrOwner ? ( {isOrgAdminOrOwner ? (
<OrgProfileForm defaultValues={defaultValues} /> <OrgProfileForm defaultValues={defaultValues} />
) : ( ) : (
<div className="border-subtle flex rounded-b-md border border-t-0 px-4 py-8 sm:px-6"> <div className="flex">
<div className="flex-grow"> <div className="flex-grow">
<div> <div>
<Label className="text-emphasis">{t("org_name")}</Label> <Label className="text-emphasis">{t("org_name")}</Label>

View File

@ -147,7 +147,7 @@ const PaymentForm = (props: Props) => {
formatParams: { amount: { currency: props.payment.currency } }, formatParams: { amount: { currency: props.payment.currency } },
})} })}
onChange={(e) => setHoldAcknowledged(e.target.checked)} onChange={(e) => setHoldAcknowledged(e.target.checked)}
descriptionClassName="text-info font-semibold" descriptionClassName="text-blue-900 font-semibold"
/> />
</div> </div>
)} )}

View File

@ -174,7 +174,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
return ( return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}> <Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow> <DialogContent>
<DialogHeader title={t("recordings_title")} subtitle={subtitle} /> <DialogHeader title={t("recordings_title")} subtitle={subtitle} />
{roomName ? ( {roomName ? (
<LicenseRequired> <LicenseRequired>