Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8abbb3e9 | ||
|
|
7bb42ba577 | ||
|
|
2e3b3257db | ||
|
|
d28e20e92e | ||
|
|
8b89d6635a | ||
|
|
13ec810cb9 | ||
|
|
367c8a96d8 | ||
|
|
e94c030c45 | ||
|
|
6d0c7e4214 | ||
|
|
e11cb9e3a5 | ||
|
|
809abd2f4f | ||
|
|
d1d50b0d91 | ||
|
|
fc716f5921 | ||
|
|
5348464972 | ||
|
|
3fac6e698e | ||
|
|
dcafbf672a | ||
|
|
dff89a4df8 | ||
|
|
ed04062540 | ||
|
|
23fda4a9e6 | ||
|
|
14c3028c2a | ||
|
|
dfe49e1732 | ||
|
|
6148c6baea | ||
|
|
5366d891bb | ||
|
|
2ee021a87f | ||
|
|
dd0c2dbff2 | ||
|
|
f044c2d7c7 | ||
|
|
703bcd861e | ||
|
|
d4d96eb5cd | ||
|
|
d8356e9a5b | ||
|
|
9348279818 | ||
|
|
5d77e4ce1d | ||
|
|
968e782dbe | ||
|
|
01fb3dd03b | ||
|
|
c3537f3290 | ||
|
|
3bb42f276e | ||
|
|
51fd4102ae | ||
|
|
9d1ef0a649 | ||
|
|
f80dc0738a | ||
|
|
678ab3f453 | ||
|
|
199d3e4c3f | ||
|
|
79a6aef0e7 | ||
|
|
4d49fb0636 | ||
|
|
0be1387d0f | ||
|
|
58ab278813 | ||
|
|
4de142cb7c | ||
|
|
b4d27a9326 | ||
|
|
31f3d9778e | ||
|
|
0a59c95b93 | ||
|
|
9e3465eeb6 | ||
|
|
31fc4724e0 | ||
|
|
f81f0a26ec | ||
|
|
9a80bb6194 | ||
|
|
901fc36c97 | ||
|
|
2831fb2b57 | ||
|
|
426d31712e | ||
|
|
09ecd445bb | ||
|
|
08d65c85de | ||
|
|
b9cef10ef2 | ||
|
|
0dc41592f2 | ||
|
|
aabf3c54ea | ||
|
|
c2a57fd72b | ||
|
|
52386e08f2 |
No files matched your search
@@ -115,6 +115,10 @@ SENDGRID_API_KEY=
|
|||||||
SENDGRID_EMAIL=
|
SENDGRID_EMAIL=
|
||||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
# Used for capturing exceptions and logging messages
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
|
||||||
# Twilio
|
# Twilio
|
||||||
# Used to send SMS reminders in workflows
|
# Used to send SMS reminders in workflows
|
||||||
TWILIO_SID=
|
TWILIO_SID=
|
||||||
|
|||||||
@@ -47,3 +47,9 @@ assignees: ""
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
(Share it here.)
|
(Share it here.)
|
||||||
|
|
||||||
|
---
|
||||||
|
##### House rules
|
||||||
|
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
|
||||||
|
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
|
||||||
|
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
|
||||||
@@ -40,7 +40,7 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
<!-- Remove bullet points below that don't apply to you -->
|
||||||
|
|
||||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||||
- My code doesn't follow the style guidelines of this project
|
- My code doesn't follow the style guidelines of this project
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
+12
-1
@@ -2,7 +2,18 @@
|
|||||||
|
|
||||||
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
## House rules
|
||||||
|
|
||||||
|
- Before submitting a new issue or PR, check if it already exists in [issues](https://github.com/calcom/cal.com/issues) or [PRs](https://github.com/calcom/cal.com/pulls).
|
||||||
|
- GitHub issues: take note of the `🚨 needs approval` label.
|
||||||
|
- **For Contributors**:
|
||||||
|
- Feature Requests: Wait for a core member to approve and remove the `🚨 needs approval` label before you start coding or submit a PR.
|
||||||
|
- Bugs, Security, Performance, Documentation, etc.: You can start coding immediately, even if the `🚨 needs approval` label is present. This label mainly concerns feature requests.
|
||||||
|
- **Our Process**:
|
||||||
|
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||||
|
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Priorities
|
## Priorities
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Here is what you need to be able to run Cal.com.
|
|||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales))
|
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/calcom/cal.com.git
|
git clone https://github.com/calcom/cal.com.git
|
||||||
@@ -221,7 +221,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||||
|
|
||||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
1. Set a 24 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
In a development environment, run:
|
In a development environment, run:
|
||||||
@@ -597,8 +597,6 @@ Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/ma
|
|||||||
|
|
||||||
Special thanks to these amazing projects which help power Cal.com:
|
Special thanks to these amazing projects which help power Cal.com:
|
||||||
|
|
||||||
[<img src="https://cal.com/powered-by-vercel.svg">](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
|
||||||
|
|
||||||
- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
||||||
- [Next.js](https://nextjs.org/)
|
- [Next.js](https://nextjs.org/)
|
||||||
- [Day.js](https://day.js.org/)
|
- [Day.js](https://day.js.org/)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Checkly Tests
|
||||||
|
|
||||||
|
Run as `yarn checkly test`
|
||||||
|
Deploy the tests as `yarn checkly deploy`
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Org", () => {
|
||||||
|
// Because these pages involve next.config.js rewrites, it's better to test them on production
|
||||||
|
test.describe("Embeds - i.cal.com", () => {
|
||||||
|
test("Org Profile Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await page.screenshot({ path: "screenshot.jpg" });
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/sales/hipaa/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||||
|
async function expectPageToBeServerSideRendered(page: Page) {
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function isValidBase64Image(input: string): boolean {
|
||||||
|
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||||
|
return regex.test(input);
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ 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,
|
||||||
@@ -15,7 +14,6 @@ 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(),
|
||||||
@@ -47,5 +45,4 @@ export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
|
|||||||
eventTypeId: true,
|
eventTypeId: true,
|
||||||
bookingId: true,
|
bookingId: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
credentialId: true,
|
|
||||||
});
|
});
|
||||||
@@ -4,6 +4,7 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
|||||||
import { _UserModel as User } from "@calcom/prisma/zod";
|
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
|
||||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||||
|
|
||||||
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
||||||
@@ -106,6 +107,7 @@ const schemaUserEditParams = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
locale: z.nativeEnum(locales).optional().nullable(),
|
locale: z.nativeEnum(locales).optional().nullable(),
|
||||||
|
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||||
@@ -128,6 +130,7 @@ const schemaUserCreateParams = z.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
locale: z.nativeEnum(locales).optional(),
|
locale: z.nativeEnum(locales).optional(),
|
||||||
createdDate: iso8601.optional(),
|
createdDate: iso8601.optional(),
|
||||||
|
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3002 next start",
|
"start": "PORT=3002 next start",
|
||||||
|
"docker-start-api": "PORT=80 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^7.20.0",
|
"@sentry/nextjs": "^7.73.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.4.6",
|
"next": "^13.4.6",
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* <td>The provided id didn't correspond to any existing booking.</td>
|
* <td>The provided id didn't correspond to any existing booking.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
* <tr>
|
* <tr>
|
||||||
* <td>Cannot cancel past events</td>
|
|
||||||
* <td>The provided id matched an existing booking with a past startDate.</td>
|
|
||||||
* </tr>
|
|
||||||
* <tr>
|
|
||||||
* <td>User not found</td>
|
* <td>User not found</td>
|
||||||
* <td>The userId did not matched an existing user.</td>
|
* <td>The userId did not matched an existing user.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
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,
|
||||||
@@ -56,16 +62,251 @@ 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 { prisma, query, body } = req;
|
const { userId, isAdmin, 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,
|
data: { ...parsedBody, credentialId },
|
||||||
});
|
});
|
||||||
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);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
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,
|
||||||
@@ -38,9 +40,6 @@ 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'
|
||||||
@@ -65,20 +64,38 @@ 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 || userId : userId;
|
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.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 credential = await prisma.credential.findFirst({
|
const userCredentials = await prisma.credential.findMany({
|
||||||
where: { type: parsedBody.integration, userId: assignedUserId },
|
where: {
|
||||||
select: { id: true, type: true, userId: true },
|
type: parsedBody.integration,
|
||||||
|
userId: assignedUserId,
|
||||||
|
},
|
||||||
|
select: credentialForCalendarServiceSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!credential)
|
if (userCredentials.length === 0)
|
||||||
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 },
|
||||||
@@ -91,7 +108,9 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
parsedBody.userId = undefined;
|
parsedBody.userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destination_calendar = await prisma.destinationCalendar.create({ data: { ...parsedBody } });
|
const destination_calendar = await prisma.destinationCalendar.create({
|
||||||
|
data: { ...parsedBody, credentialId },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
customInputs: true,
|
customInputs: true,
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
children: { select: { id: true, userId: true } },
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
customInputs: true,
|
customInputs: true,
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
children: { select: { id: true, userId: true } },
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -316,8 +316,9 @@ async function checkPermissions(req: NextApiRequest) {
|
|||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `userId`",
|
message: "ADMIN required for `userId`",
|
||||||
});
|
});
|
||||||
/* Admin users are required to pass in a userId */
|
/* Admin users are required to pass in a userId or teamId */
|
||||||
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
if (isAdmin && (!body.userId || !body.teamId))
|
||||||
|
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(postHandler);
|
export default defaultResponder(postHandler);
|
||||||
@@ -65,6 +65,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* locale:
|
* locale:
|
||||||
* description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
* description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||||
* type: string
|
* type: string
|
||||||
|
* avatar:
|
||||||
|
* description: The user's avatar, in base64 format
|
||||||
|
* type: string
|
||||||
* examples:
|
* examples:
|
||||||
* user:
|
* user:
|
||||||
* summary: An example of USER
|
* summary: An example of USER
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
|||||||
* locale:
|
* locale:
|
||||||
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||||
* type: string
|
* type: string
|
||||||
|
* avatar:
|
||||||
|
* description: The user's avatar, in base64 format
|
||||||
|
* type: string
|
||||||
* examples:
|
* examples:
|
||||||
* user:
|
* user:
|
||||||
* summary: An example of USER
|
* summary: An example of USER
|
||||||
|
|||||||
@@ -631,8 +631,7 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
sendEmailInvitation:
|
|
||||||
type: boolean
|
|
||||||
parameters:
|
parameters:
|
||||||
- schema:
|
- schema:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
hidePlaceholder
|
hidePlaceholder
|
||||||
isLoading={mutation.isLoading}
|
isLoading={mutation.isLoading}
|
||||||
value={data.destinationCalendar?.externalId}
|
value={data.destinationCalendar?.externalId}
|
||||||
|
hideAdvancedText
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -141,17 +141,6 @@ 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",
|
||||||
@@ -270,11 +259,19 @@ 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 =
|
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed);
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
|
||||||
|
const showRecordingActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: "view_recordings",
|
||||||
|
label: t("view_recordings"),
|
||||||
|
onClick: () => {
|
||||||
|
setViewRecordingsDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -356,9 +356,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
locationFormMethods.setValue("locationType", val.value);
|
||||||
if (val.credential) {
|
if (!!val.credentialId) {
|
||||||
locationFormMethods.setValue("credentialId", val.credential.id);
|
locationFormMethods.setValue("credentialId", val.credentialId);
|
||||||
locationFormMethods.setValue("teamName", val.credential.team?.name);
|
locationFormMethods.setValue("teamName", val.teamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
locationFormMethods.unregister([
|
locationFormMethods.unregister([
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
showToast,
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Plus, X, Check } from "@calcom/ui/components/icon";
|
import { Plus, X, Check, CornerDownRight } from "@calcom/ui/components/icon";
|
||||||
import { CornerDownRight } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
|
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
|
||||||
@@ -200,23 +199,16 @@ export const EventSetupTab = (
|
|||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Input
|
||||||
<Input
|
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
className="my-0"
|
className="my-0"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<ErrorMessage
|
|
||||||
errors={formMethods.formState.errors.locations?.[index]}
|
|
||||||
name={eventLocationType.defaultValueVariable}
|
|
||||||
className="text-error my-1 text-sm"
|
|
||||||
as="div"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -231,21 +223,14 @@ export const EventSetupTab = (
|
|||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<PhoneInput
|
||||||
<PhoneInput
|
required
|
||||||
required
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<ErrorMessage
|
|
||||||
errors={formMethods.formState.errors.locations?.[index]}
|
|
||||||
name={eventLocationType.defaultValueVariable}
|
|
||||||
className="text-error my-1 text-sm"
|
|
||||||
as="div"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -298,9 +283,21 @@ export const EventSetupTab = (
|
|||||||
!validLocations.find((location) => location.type === newLocationType);
|
!validLocations.find((location) => location.type === newLocationType);
|
||||||
|
|
||||||
if (canAddLocation) {
|
if (canAddLocation) {
|
||||||
updateLocationField(index, { type: newLocationType });
|
updateLocationField(index, {
|
||||||
|
type: newLocationType,
|
||||||
|
...(e.credentialId && {
|
||||||
|
credentialId: e.credentialId,
|
||||||
|
teamName: e.teamName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
updateLocationField(index, { type: field.type });
|
updateLocationField(index, {
|
||||||
|
type: field.type,
|
||||||
|
...(field.credentialId && {
|
||||||
|
credentialId: field.credentialId,
|
||||||
|
teamName: field.teamName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
showToast(t("location_already_exists"), "warning");
|
showToast(t("location_already_exists"), "warning");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,11 +317,11 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
{eventLocationType?.organizerInputType && (
|
{eventLocationType?.organizerInputType && (
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="w-full">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex gap-2">
|
||||||
<CornerDownRight className="h-4 w-4" />
|
<div className="flex items-center justify-center">
|
||||||
</div>
|
<CornerDownRight className="h-4 w-4" />
|
||||||
<div className="w-full">
|
</div>
|
||||||
<LocationInput
|
<LocationInput
|
||||||
defaultValue={
|
defaultValue={
|
||||||
defaultLocation
|
defaultLocation
|
||||||
@@ -335,6 +332,12 @@ export const EventSetupTab = (
|
|||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ErrorMessage
|
||||||
|
errors={formMethods.formState.errors.locations?.[index]}
|
||||||
|
name={eventLocationType.defaultValueVariable}
|
||||||
|
className="text-error my-1 ml-6 text-sm"
|
||||||
|
as="div"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-6">
|
<div className="ml-6">
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
@@ -382,7 +385,13 @@ export const EventSetupTab = (
|
|||||||
!validLocations.find((location) => location.type === newLocationType);
|
!validLocations.find((location) => location.type === newLocationType);
|
||||||
|
|
||||||
if (canAppendLocation) {
|
if (canAppendLocation) {
|
||||||
append({ type: newLocationType });
|
append({
|
||||||
|
type: newLocationType,
|
||||||
|
...(e.credentialId && {
|
||||||
|
credentialId: e.credentialId,
|
||||||
|
teamName: e.teamName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
setSelectedNewOption(e);
|
setSelectedNewOption(e);
|
||||||
} else {
|
} else {
|
||||||
showToast(t("location_already_exists"), "warning");
|
showToast(t("location_already_exists"), "warning");
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { GroupBase, Props, SingleValue } from "react-select";
|
|||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
|
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
|
||||||
import { Select } from "@calcom/ui";
|
import { Select } from "@calcom/ui";
|
||||||
@@ -13,7 +12,8 @@ export type LocationOption = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
address?: string;
|
address?: string;
|
||||||
credential?: CredentialDataWithTeamName;
|
credentialId?: number;
|
||||||
|
teamName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
|
|||||||
const os = require("os");
|
const os = require("os");
|
||||||
const englishTranslation = require("./public/static/locales/en/common.json");
|
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
const { version } = require("./package.json");
|
const { version } = require("./package.json");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
const {
|
const {
|
||||||
@@ -92,6 +93,7 @@ if (process.env.ANALYZE === "true") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins.push(withAxiom);
|
plugins.push(withAxiom);
|
||||||
|
|
||||||
const matcherConfigRootPath = {
|
const matcherConfigRootPath = {
|
||||||
has: [
|
has: [
|
||||||
{
|
{
|
||||||
@@ -102,6 +104,16 @@ const matcherConfigRootPath = {
|
|||||||
source: "/",
|
source: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const matcherConfigRootPathEmbed = {
|
||||||
|
has: [
|
||||||
|
{
|
||||||
|
type: "host",
|
||||||
|
value: orgHostPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: "/embed",
|
||||||
|
};
|
||||||
|
|
||||||
const matcherConfigUserRoute = {
|
const matcherConfigUserRoute = {
|
||||||
has: [
|
has: [
|
||||||
{
|
{
|
||||||
@@ -245,6 +257,10 @@ const nextConfig = {
|
|||||||
...matcherConfigRootPath,
|
...matcherConfigRootPath,
|
||||||
destination: "/team/:orgSlug?isOrgProfile=1",
|
destination: "/team/:orgSlug?isOrgProfile=1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...matcherConfigRootPathEmbed,
|
||||||
|
destination: "/team/:orgSlug/embed?isOrgProfile=1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...matcherConfigUserRoute,
|
...matcherConfigUserRoute,
|
||||||
destination: "/org/:orgSlug/:user",
|
destination: "/org/:orgSlug/:user",
|
||||||
@@ -533,4 +549,13 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
nextConfig["sentry"] = {
|
||||||
|
autoInstrumentServerFunctions: true,
|
||||||
|
hideSourceMaps: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
plugins.push(withSentryConfig);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.4.5",
|
"version": "3.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
|
"@sentry/nextjs": "^7.73.0",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import Link from "next/link";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import {
|
||||||
|
getOrgDomainConfigFromHostname,
|
||||||
|
subdomainSuffix,
|
||||||
|
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
@@ -50,7 +53,10 @@ export default function Custom404() {
|
|||||||
|
|
||||||
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
|
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
|
const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({
|
||||||
|
hostname: window.location.host,
|
||||||
|
});
|
||||||
|
|
||||||
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
||||||
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
|
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
|
||||||
const splitPath = routerUsername.split("/");
|
const splitPath = routerUsername.split("/");
|
||||||
|
|||||||
@@ -275,10 +275,7 @@ export type UserPageProps = {
|
|||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const usernameList = getUsernameList(context.query.user as string);
|
const usernameList = getUsernameList(context.query.user as string);
|
||||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||||
const dataFetchStart = Date.now();
|
const dataFetchStart = Date.now();
|
||||||
|
|||||||
@@ -72,10 +72,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -148,10 +145,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const username = usernames[0];
|
const username = usernames[0];
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
|
|||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const parsedQuery = logoApiSchema.parse(query);
|
const parsedQuery = logoApiSchema.parse(query);
|
||||||
const { isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { isValidOrgDomain } = orgDomainConfig(req);
|
||||||
|
|
||||||
const hostname = req?.headers["host"];
|
const hostname = req?.headers["host"];
|
||||||
if (!hostname) throw new Error("No hostname");
|
if (!hostname) throw new Error("No hostname");
|
||||||
|
|||||||
@@ -62,6 +62,46 @@ 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" });
|
||||||
@@ -137,12 +177,22 @@ 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((attendee) => attendee.id === session.user.id);
|
attendeesList.find(
|
||||||
|
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
|
||||||
|
);
|
||||||
|
|
||||||
if (!isUserAttendeeOrOrganiser) {
|
if (!isUserAttendeeOrOrganiser) {
|
||||||
return res.status(403).send({
|
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
||||||
message: "Unauthorised",
|
booking?.eventType?.teamId,
|
||||||
});
|
session.user.id,
|
||||||
|
session.user.email
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserMemberOfTheTeam) {
|
||||||
|
return res.status(403).send({
|
||||||
|
message: "Unauthorised",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.booking.update({
|
await prisma.booking.update({
|
||||||
@@ -202,7 +252,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("something_went_wrong", err);
|
console.warn("Error in /recorded-daily-video", err);
|
||||||
return res.status(500).json({ message: "something went wrong" });
|
return res.status(500).json({ message: "something went wrong" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const querySchema = z
|
|||||||
|
|
||||||
async function getIdentityData(req: NextApiRequest) {
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req);
|
||||||
|
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type Response = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
||||||
const { currentOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { currentOrgDomain } = orgDomainConfig(req);
|
||||||
const result = await checkUsername(req.body.username, currentOrgDomain);
|
const result = await checkUsername(req.body.username, currentOrgDomain);
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
||||||
|
|||||||
@@ -342,14 +342,17 @@ 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:block sm:p-0"
|
"flex items-end justify-center px-4 pb-20 pt-4 sm:flex sm:p-0"
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
|
className={classNames(
|
||||||
|
"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(
|
||||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
"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"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
const session = await getServerSession(context);
|
const session = await getServerSession(context);
|
||||||
const { link, slug } = paramsSchema.parse(context.params);
|
const { link, slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import { validateCustomEventName } from "@calcom/core/event";
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
import type { EventLocationType } from "@calcom/core/location";
|
import type { EventLocationType } from "@calcom/core/location";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
@@ -321,6 +322,47 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
teamName: z.string().optional(),
|
teamName: z.string().optional(),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (val?.link) {
|
||||||
|
const link = val.link;
|
||||||
|
const eventLocationType = getEventLocationType(val.type);
|
||||||
|
if (
|
||||||
|
eventLocationType &&
|
||||||
|
!eventLocationType.default &&
|
||||||
|
eventLocationType.linkType === "static" &&
|
||||||
|
eventLocationType.urlRegExp
|
||||||
|
) {
|
||||||
|
const valid = z
|
||||||
|
.string()
|
||||||
|
.regex(new RegExp(eventLocationType.urlRegExp))
|
||||||
|
.safeParse(link).success;
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||||
|
message: t("invalid_url_error_message", {
|
||||||
|
label: eventLocationType.label,
|
||||||
|
sampleUrl: sampleUrl ?? "https://cal.com",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = z.string().url().optional().safeParse(link).success;
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||||
|
message: `Invalid URL`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||||
|
|
||||||
|
export { default } from "./index";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -283,8 +283,8 @@ const ProfileView = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="border-subtle mt-6 rounded-lg rounded-b-none border border-b-0 p-6">
|
<div className="border-subtle mt-6 rounded-lg rounded-b-none border border-b-0 p-6">
|
||||||
<Label className="text-base font-semibold text-red-700">{t("danger_zone")}</Label>
|
<Label className="mb-0 text-base font-semibold text-red-700">{t("danger_zone")}</Label>
|
||||||
<p className="text-subtle">{t("account_deletion_cannot_be_undone")}</p>
|
<p className="text-subtle text-sm">{t("account_deletion_cannot_be_undone")}</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Delete account Dialog */}
|
{/* Delete account Dialog */}
|
||||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||||
|
|||||||
@@ -164,14 +164,13 @@ 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>
|
<div className="border-subtle rounded-b-xl border border-t-0 px-4 py-6 sm:px-6">
|
||||||
<div className="mt-6">
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
{t("account_managed_by_identity_provider", {
|
||||||
{t("account_managed_by_identity_provider", {
|
provider: identityProviderNameMap[user.identityProvider],
|
||||||
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],
|
||||||
@@ -180,7 +179,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Form form={formMethods} handleSubmit={handleSubmit}>
|
<Form form={formMethods} handleSubmit={handleSubmit}>
|
||||||
<div className="border-x px-4 py-6 sm:px-6">
|
<div className="border-subtle 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} />
|
||||||
|
|||||||
@@ -269,10 +269,7 @@ function TeamPage({
|
|||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
|
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||||
|
|
||||||
// Provided by Rewrite from next.config.js
|
// Provided by Rewrite from next.config.js
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
|
|
||||||
if (!isOrgContext) {
|
if (!isOrgContext) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
import { loginUser } from "../fixtures/regularBookings";
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.describe("Booking With Checkbox Group 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 Checkbox Group Question and Address Question", () => {
|
||||||
|
test("Checkbox Group required and Address required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group 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("Checkbox Group and Address not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Address question (only checkbox 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 Checkbox Group Question and Phone Question", () => {
|
||||||
|
test("Checkbox Group required and Phone required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group 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("Checkbox Group and Phone not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Phone question (only checkbox required)",
|
||||||
|
secondQuestion: "phone",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Checkbox Group Question and checkbox Question", () => {
|
||||||
|
test("Checkbox Group required and checkbox required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and checkbox question (both required)",
|
||||||
|
secondQuestion: "boolean",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Checkbox Group and checkbox not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and checkbox (only checkbox 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 Checkbox Group Question and Long text Question", () => {
|
||||||
|
test("Checkbox Group required and Long text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Long Text question (both required)",
|
||||||
|
secondQuestion: "textarea",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Checkbox Group and Long text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Long Text question (only checkbox 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 Checkbox Group Question and Multi email Question", () => {
|
||||||
|
test("Checkbox Group required and Multi email required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Multi Email question (both required)",
|
||||||
|
secondQuestion: "multiemail",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Checkbox Group and Multi email not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Multi Email question (only checkbox 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 Checkbox Group Question and multiselect Question", () => {
|
||||||
|
test("Checkbox Group required and multiselect text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Multi Select question (both required)",
|
||||||
|
secondQuestion: "multiselect",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Checkbox Group and multiselect text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Multi Select question (only checkbox 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 Checkbox Group Question and Number Question", () => {
|
||||||
|
test("Checkbox Group required and Number required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group 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("Checkbox Group and Number not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Number question (only checkbox 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 Checkbox Group Question and Radio group Question", () => {
|
||||||
|
test("Checkbox Group required and Radio group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group 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("Checkbox Group and Radio group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Radio question (only checkbox 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 Checkbox Group Question and select Question", () => {
|
||||||
|
test("Checkbox Group required and select required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Select question (both required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Checkbox Group and select not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Select question (only checkbox required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Checkbox Group Question and Short text question", () => {
|
||||||
|
test("Checkbox Group required and Short Text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group 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("Checkbox Group and Short Text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
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: "checkbox",
|
||||||
|
fillText: "Test Checkbox Group question and Text question (only checkbox required)",
|
||||||
|
secondQuestion: "text",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
import { loginUser } from "../fixtures/regularBookings";
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.describe("Booking With Multiple Email 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 Multiple Email Question and Address Question", () => {
|
||||||
|
test("Multiple Email required and Address required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email 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("Multiple Email and Address not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Address question (only Multiple Email 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 Multiple Email Question and checkbox group Question", () => {
|
||||||
|
test("Multiple Email required and checkbox group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and checkbox group question (both required)",
|
||||||
|
secondQuestion: "checkbox",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple Email and checkbox group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and checkbox group question (only Multiple Email 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 Multiple Email Question and checkbox Question", () => {
|
||||||
|
test("Multiple Email required and checkbox required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and checkbox question (both required)",
|
||||||
|
secondQuestion: "boolean",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple Email and checkbox not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and checkbox (only Multiple Email 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 Multiple Email Question and Long text Question", () => {
|
||||||
|
test("Multiple Email required and Long text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Long Text question (both required)",
|
||||||
|
secondQuestion: "textarea",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple Email and Long text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Long Text question (only Multiple Email 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 Multiple Email Question and multiselect Question", () => {
|
||||||
|
test("Multiple Email required and multiselect text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Multi Select question (both required)",
|
||||||
|
secondQuestion: "multiselect",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple Email and multiselect text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Multi Select question (only Multiple Email 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 Multiple Email Question and Number Question", () => {
|
||||||
|
test("Multiple Email required and Number required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email 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("Multiple Email and Number not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Number question (only Multiple Email 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 Multiple email Question and phone Question", () => {
|
||||||
|
test("Multiple email required and Phone required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email 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("Multiple email and Phone not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Phone question (both required)",
|
||||||
|
secondQuestion: "phone",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Multiple Email Question and Radio group Question", () => {
|
||||||
|
test("Multiple Email required and Radio group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email 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("Multiple Email and Radio group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Radio question (only Multiple Email 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 Multiple Email Question and select Question", () => {
|
||||||
|
test("Multiple Email required and select required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Select question (both required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple Email and select not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Select question (only Multiple Email required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Multiple Email Question and Short text question", () => {
|
||||||
|
test("Multiple Email required and Short text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email 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("Multiple Email and Short text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail 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: "multiemail",
|
||||||
|
fillText: "Test Multiple Email question and Text question (only Multiple Email required)",
|
||||||
|
secondQuestion: "text",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -402,7 +402,6 @@ test.describe("Booking With Phone Question and Each Other Question", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Booking With Phone Question and Short text question", () => {
|
test.describe("Booking With Phone Question and Short text question", () => {
|
||||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
|
||||||
test("Phone and Short text required", async ({ bookingPage }) => {
|
test("Phone and Short text required", async ({ bookingPage }) => {
|
||||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import { loginUser } from "../fixtures/regularBookings";
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.describe("Booking With Radio 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 Radio Question and Address Question", () => {
|
||||||
|
test("Radio required and Address required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio 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("Radio and Address not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Address question (only radio 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 Radio Question and checkbox group Question", () => {
|
||||||
|
test("Radio required and checkbox group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and checkbox group question (both required)",
|
||||||
|
secondQuestion: "checkbox",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Radio and checkbox group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and checkbox group question (only radio 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 Radio Question and checkbox Question", () => {
|
||||||
|
test("Radio required and checkbox required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and checkbox question (both required)",
|
||||||
|
secondQuestion: "boolean",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
test("Radio and checkbox not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and checkbox (only radio 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 Radio Question and Long text Question", () => {
|
||||||
|
test("Radio required and Long text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Long Text question (both required)",
|
||||||
|
secondQuestion: "textarea",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Radio and Long text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Long Text question (only radio 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 Radio Question and Multi email Question", () => {
|
||||||
|
test("Radio required and Multi email required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Multi Email question (both required)",
|
||||||
|
secondQuestion: "multiemail",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Radio and Multi email not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Multi Email question (only radio 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 Radio Question and multiselect Question", () => {
|
||||||
|
test("Radio required and multiselect text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Multi Select question (both required)",
|
||||||
|
secondQuestion: "multiselect",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Radio and multiselect text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Multi Select question (only radio 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 Radio Question and Number Question", () => {
|
||||||
|
test("Radio required and Number required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio 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("Radio and Number not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Number question (only radio 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 Radio Question and Phone Question", () => {
|
||||||
|
test("Radio required and Phone required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio 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("Radio and Phone not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Phone question (only radio required)",
|
||||||
|
secondQuestion: "phone",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Radio Question and select Question", () => {
|
||||||
|
test("Radio required and select required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Select question (both required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Radio and select not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Select question (only radio required)",
|
||||||
|
secondQuestion: "select",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Radio Question and Short text question", () => {
|
||||||
|
test("Radio required and Short text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio 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("Radio and Short text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
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: "radio",
|
||||||
|
fillText: "Test Radio question and Text question (only radio required)",
|
||||||
|
secondQuestion: "text",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import type { Team } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
|
||||||
|
|
||||||
|
// creates a user fixture instance and stores the collection
|
||||||
|
export const createOrgsFixture = (page: Page) => {
|
||||||
|
const store = { orgs: [], page } as { orgs: Team[]; page: typeof page };
|
||||||
|
return {
|
||||||
|
create: async (opts: { name: string; slug?: string; requestedSlug?: string }) => {
|
||||||
|
const org = await createOrgInDb({
|
||||||
|
name: opts.name,
|
||||||
|
slug: opts.slug || getRandomSlug(),
|
||||||
|
requestedSlug: opts.requestedSlug,
|
||||||
|
});
|
||||||
|
store.orgs.push(org);
|
||||||
|
return org;
|
||||||
|
},
|
||||||
|
get: () => store.orgs,
|
||||||
|
deleteAll: async () => {
|
||||||
|
await prisma.team.deleteMany({ where: { id: { in: store.orgs.map((org) => org.id) } } });
|
||||||
|
store.orgs = [];
|
||||||
|
},
|
||||||
|
delete: async (id: number) => {
|
||||||
|
await prisma.team.delete({ where: { id } });
|
||||||
|
store.orgs = store.orgs.filter((b) => b.id !== id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createOrgInDb({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
requestedSlug,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
requestedSlug?: string;
|
||||||
|
}) {
|
||||||
|
return await prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
slug: slug,
|
||||||
|
metadata: {
|
||||||
|
isOrganization: true,
|
||||||
|
...(requestedSlug
|
||||||
|
? {
|
||||||
|
requestedSlug,
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
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";
|
||||||
@@ -13,6 +15,7 @@ type BookingOptions = {
|
|||||||
hasPlaceholder?: boolean;
|
hasPlaceholder?: boolean;
|
||||||
isReschedule?: boolean;
|
isReschedule?: boolean;
|
||||||
isRequired?: boolean;
|
isRequired?: boolean;
|
||||||
|
isAllRequired?: boolean;
|
||||||
isMultiSelect?: boolean;
|
isMultiSelect?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,6 +41,12 @@ 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 () => {
|
||||||
@@ -103,17 +112,82 @@ 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) => {
|
||||||
@@ -154,22 +228,17 @@ export function createBookingPageFixture(page: Page) {
|
|||||||
return eventtypePromise;
|
return eventtypePromise;
|
||||||
},
|
},
|
||||||
selectTimeSlot: async (eventTypePage: Page) => {
|
selectTimeSlot: async (eventTypePage: Page) => {
|
||||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||||
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();
|
||||||
},
|
},
|
||||||
|
|
||||||
fillRescheduleReasonAndConfirm: async () => {
|
fillRescheduleReasonAndConfirm: async () => {
|
||||||
await page.getByPlaceholder(reschedulePlaceholderText).click();
|
await page.getByPlaceholder(reschedulePlaceholderText).click();
|
||||||
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
|
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
|
||||||
@@ -186,6 +255,7 @@ 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();
|
||||||
@@ -194,6 +264,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) => {
|
||||||
@@ -222,7 +300,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"].includes(secondQuestion);
|
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion);
|
||||||
|
|
||||||
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
||||||
question === "radio" && secondQuestion === "checkbox";
|
question === "radio" && secondQuestion === "checkbox";
|
||||||
@@ -248,6 +326,32 @@ export function createBookingPageFixture(page: Page) {
|
|||||||
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
|
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
|
||||||
|
|
||||||
await eventTypePage.getByTestId(confirmButton).click();
|
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 fillQuestion(eventTypePage, question, customLocators);
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
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);
|
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
|
||||||
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
|
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
|
||||||
await expect(scheduleSuccessfullyPage).toBeVisible();
|
await expect(scheduleSuccessfullyPage).toBeVisible();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/avail
|
|||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
|
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
|
||||||
import { TimeZoneEnum } from "./types";
|
import { TimeZoneEnum } from "./types";
|
||||||
@@ -78,11 +79,13 @@ const createTeamAndAddUser = async (
|
|||||||
isUnpublished,
|
isUnpublished,
|
||||||
isOrg,
|
isOrg,
|
||||||
hasSubteam,
|
hasSubteam,
|
||||||
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
user: { id: number; username: string | null; role?: MembershipRole };
|
user: { id: number; username: string | null; role?: MembershipRole };
|
||||||
isUnpublished?: boolean;
|
isUnpublished?: boolean;
|
||||||
isOrg?: boolean;
|
isOrg?: boolean;
|
||||||
hasSubteam?: true;
|
hasSubteam?: true;
|
||||||
|
organizationId?: number | null;
|
||||||
},
|
},
|
||||||
workerInfo: WorkerInfo
|
workerInfo: WorkerInfo
|
||||||
) => {
|
) => {
|
||||||
@@ -101,6 +104,7 @@ const createTeamAndAddUser = async (
|
|||||||
data.children = { connect: [{ id: team.id }] };
|
data.children = { connect: [{ id: team.id }] };
|
||||||
}
|
}
|
||||||
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
|
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
|
||||||
|
data.parent = organizationId ? { connect: { id: organizationId } } : undefined;
|
||||||
const team = await prisma.team.create({
|
const team = await prisma.team.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -114,6 +118,7 @@ const createTeamAndAddUser = async (
|
|||||||
accepted: true,
|
accepted: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return team;
|
return team;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -282,6 +287,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||||||
isUnpublished: scenario.isUnpublished,
|
isUnpublished: scenario.isUnpublished,
|
||||||
isOrg: scenario.isOrg,
|
isOrg: scenario.isOrg,
|
||||||
hasSubteam: scenario.hasSubteam,
|
hasSubteam: scenario.hasSubteam,
|
||||||
|
organizationId: opts?.organizationId,
|
||||||
},
|
},
|
||||||
workerInfo
|
workerInfo
|
||||||
);
|
);
|
||||||
@@ -399,11 +405,27 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||||||
logout: async () => {
|
logout: async () => {
|
||||||
await page.goto("/auth/logout");
|
await page.goto("/auth/logout");
|
||||||
},
|
},
|
||||||
getTeam: async () => {
|
getFirstTeam: async () => {
|
||||||
return prisma.membership.findFirstOrThrow({
|
const memberships = await prisma.membership.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
include: { team: true },
|
include: { team: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const membership = memberships
|
||||||
|
.map((membership) => {
|
||||||
|
return {
|
||||||
|
...membership,
|
||||||
|
team: {
|
||||||
|
...membership.team,
|
||||||
|
metadata: teamMetadataSchema.parse(membership.team.metadata),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.find((membership) => !membership.team?.metadata?.isOrganization);
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error("No team found for user");
|
||||||
|
}
|
||||||
|
return membership;
|
||||||
},
|
},
|
||||||
getOrg: async () => {
|
getOrg: async () => {
|
||||||
return prisma.membership.findFirstOrThrow({
|
return prisma.membership.findFirstOrThrow({
|
||||||
@@ -453,16 +475,27 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||||||
type SupportedTestEventTypes = PrismaType.EventTypeCreateInput & {
|
type SupportedTestEventTypes = PrismaType.EventTypeCreateInput & {
|
||||||
_bookings?: PrismaType.BookingCreateInput[];
|
_bookings?: PrismaType.BookingCreateInput[];
|
||||||
};
|
};
|
||||||
type CustomUserOptsKeys = "username" | "password" | "completedOnboarding" | "locale" | "name" | "email";
|
type CustomUserOptsKeys =
|
||||||
|
| "username"
|
||||||
|
| "password"
|
||||||
|
| "completedOnboarding"
|
||||||
|
| "locale"
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "organizationId";
|
||||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||||
timeZone?: TimeZoneEnum;
|
timeZone?: TimeZoneEnum;
|
||||||
eventTypes?: SupportedTestEventTypes[];
|
eventTypes?: SupportedTestEventTypes[];
|
||||||
// ignores adding the worker-index after username
|
// ignores adding the worker-index after username
|
||||||
useExactUsername?: boolean;
|
useExactUsername?: boolean;
|
||||||
|
roleInOrganization?: MembershipRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
// creates the actual user in the db.
|
// creates the actual user in the db.
|
||||||
const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): PrismaType.UserCreateInput => {
|
const createUser = (
|
||||||
|
workerInfo: WorkerInfo,
|
||||||
|
opts?: CustomUserOpts | null
|
||||||
|
): PrismaType.UserUncheckedCreateInput => {
|
||||||
// build a unique name for our user
|
// build a unique name for our user
|
||||||
const uname =
|
const uname =
|
||||||
opts?.useExactUsername && opts?.username
|
opts?.useExactUsername && opts?.username
|
||||||
@@ -478,6 +511,7 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
|
|||||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||||
locale: opts?.locale ?? "en",
|
locale: opts?.locale ?? "en",
|
||||||
|
...getOrganizationRelatedProps({ organizationId: opts?.organizationId, role: opts?.roleInOrganization }),
|
||||||
schedules:
|
schedules:
|
||||||
opts?.completedOnboarding ?? true
|
opts?.completedOnboarding ?? true
|
||||||
? {
|
? {
|
||||||
@@ -493,6 +527,42 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getOrganizationRelatedProps({
|
||||||
|
organizationId,
|
||||||
|
role,
|
||||||
|
}: {
|
||||||
|
organizationId: number | null | undefined;
|
||||||
|
role: MembershipRole | undefined;
|
||||||
|
}) {
|
||||||
|
if (!organizationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("Missing role for user in organization");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
organizationId: organizationId || null,
|
||||||
|
...(organizationId
|
||||||
|
? {
|
||||||
|
teams: {
|
||||||
|
// Create membership
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
connect: {
|
||||||
|
id: organizationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accepted: true,
|
||||||
|
role: MembershipRole.ADMIN,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function confirmPendingPayment(page: Page) {
|
async function confirmPendingPayment(page: Page) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import prisma from "@calcom/prisma";
|
|||||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||||
import { createBookingsFixture } from "../fixtures/bookings";
|
import { createBookingsFixture } from "../fixtures/bookings";
|
||||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||||
|
import { createOrgsFixture } from "../fixtures/orgs";
|
||||||
import { createPaymentsFixture } from "../fixtures/payments";
|
import { createPaymentsFixture } from "../fixtures/payments";
|
||||||
import { createBookingPageFixture } from "../fixtures/regularBookings";
|
import { createBookingPageFixture } from "../fixtures/regularBookings";
|
||||||
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
||||||
@@ -17,6 +18,7 @@ import { createUsersFixture } from "../fixtures/users";
|
|||||||
|
|
||||||
export interface Fixtures {
|
export interface Fixtures {
|
||||||
page: Page;
|
page: Page;
|
||||||
|
orgs: ReturnType<typeof createOrgsFixture>;
|
||||||
users: ReturnType<typeof createUsersFixture>;
|
users: ReturnType<typeof createUsersFixture>;
|
||||||
bookings: ReturnType<typeof createBookingsFixture>;
|
bookings: ReturnType<typeof createBookingsFixture>;
|
||||||
payments: ReturnType<typeof createPaymentsFixture>;
|
payments: ReturnType<typeof createPaymentsFixture>;
|
||||||
@@ -48,6 +50,10 @@ declare global {
|
|||||||
* @see https://playwright.dev/docs/test-fixtures
|
* @see https://playwright.dev/docs/test-fixtures
|
||||||
*/
|
*/
|
||||||
export const test = base.extend<Fixtures>({
|
export const test = base.extend<Fixtures>({
|
||||||
|
orgs: async ({ page }, use) => {
|
||||||
|
const orgsFixture = createOrgsFixture(page);
|
||||||
|
await use(orgsFixture);
|
||||||
|
},
|
||||||
users: async ({ page, context, emails }, use, workerInfo) => {
|
users: async ({ page, context, emails }, use, workerInfo) => {
|
||||||
const usersFixture = createUsersFixture(page, emails, workerInfo);
|
const usersFixture = createUsersFixture(page, emails, workerInfo);
|
||||||
await use(usersFixture);
|
await use(usersFixture);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth, testName, todo } from "./lib/testUtils";
|
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth, testName, todo } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.describe("Teams - NonOrg", () => {
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
test.describe("Teams", () => {
|
|
||||||
test("Can create teams via Wizard", async ({ page, users }) => {
|
test("Can create teams via Wizard", async ({ page, users }) => {
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||||
@@ -64,6 +64,7 @@ test.describe("Teams", () => {
|
|||||||
// await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
// await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
@@ -78,7 +79,7 @@ test.describe("Teams", () => {
|
|||||||
teammates: teamMatesObj,
|
teammates: teamMatesObj,
|
||||||
schedulingType: SchedulingType.COLLECTIVE,
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
});
|
});
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
@@ -99,6 +100,7 @@ test.describe("Teams", () => {
|
|||||||
|
|
||||||
// TODO: Assert whether the user received an email
|
// TODO: Assert whether the user received an email
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
@@ -113,7 +115,7 @@ test.describe("Teams", () => {
|
|||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
@@ -135,6 +137,7 @@ test.describe("Teams", () => {
|
|||||||
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||||
// TODO: Assert whether the user received an email
|
// TODO: Assert whether the user received an email
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Non admin team members cannot create team in org", async ({ page, users }) => {
|
test("Non admin team members cannot create team in org", async ({ page, users }) => {
|
||||||
const teamMateName = "teammate-1";
|
const teamMateName = "teammate-1";
|
||||||
|
|
||||||
@@ -169,6 +172,7 @@ test.describe("Teams", () => {
|
|||||||
await prisma.team.delete({ where: { id: org.teamId } });
|
await prisma.team.delete({ where: { id: org.teamId } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create team with same name as user", async ({ page, users }) => {
|
test("Can create team with same name as user", async ({ page, users }) => {
|
||||||
// Name to be used for both user and team
|
// Name to be used for both user and team
|
||||||
const uniqueName = "test-unique-name";
|
const uniqueName = "test-unique-name";
|
||||||
@@ -210,6 +214,7 @@ test.describe("Teams", () => {
|
|||||||
await prisma.team.delete({ where: { id: team?.id } });
|
await prisma.team.delete({ where: { id: team?.id } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a private team", async ({ page, users }) => {
|
test("Can create a private team", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
@@ -226,7 +231,7 @@ test.describe("Teams", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await owner.apiLogin();
|
await owner.apiLogin();
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
|
|
||||||
// Mark team as private
|
// Mark team as private
|
||||||
await page.goto(`/settings/teams/${team.id}/members`);
|
await page.goto(`/settings/teams/${team.id}/members`);
|
||||||
@@ -247,3 +252,180 @@ test.describe("Teams", () => {
|
|||||||
todo("Reschedule a Collective EventType booking");
|
todo("Reschedule a Collective EventType booking");
|
||||||
todo("Reschedule a Round Robin EventType booking");
|
todo("Reschedule a Round Robin EventType booking");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Teams - Org", () => {
|
||||||
|
test.afterEach(({ orgs, users }) => {
|
||||||
|
orgs.deleteAll();
|
||||||
|
users.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
|
||||||
|
const org = await orgs.create({
|
||||||
|
name: "TestOrg",
|
||||||
|
});
|
||||||
|
const user = await users.create({
|
||||||
|
organizationId: org.id,
|
||||||
|
roleInOrganization: MembershipRole.ADMIN,
|
||||||
|
});
|
||||||
|
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||||
|
await user.apiLogin();
|
||||||
|
await page.goto("/teams");
|
||||||
|
|
||||||
|
await test.step("Can create team", async () => {
|
||||||
|
// Click text=Create Team
|
||||||
|
await page.locator("text=Create a new Team").click();
|
||||||
|
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
|
||||||
|
// Fill input[name="name"]
|
||||||
|
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
|
||||||
|
// Click text=Continue
|
||||||
|
await page.locator("text=Continue").click();
|
||||||
|
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
|
||||||
|
await page.waitForSelector('[data-testid="pending-member-list"]');
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can add members", async () => {
|
||||||
|
// Click [data-testid="new-member-button"]
|
||||||
|
await page.locator('[data-testid="new-member-button"]').click();
|
||||||
|
// Fill [placeholder="email\@example\.com"]
|
||||||
|
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
|
||||||
|
// Click [data-testid="invite-new-member-button"]
|
||||||
|
await page.locator('[data-testid="invite-new-member-button"]').click();
|
||||||
|
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can remove members", async () => {
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
|
||||||
|
|
||||||
|
const lastRemoveMemberButton = page.locator('[data-testid="remove-member-button"]').last();
|
||||||
|
await lastRemoveMemberButton.click();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(1);
|
||||||
|
|
||||||
|
// Cleanup here since this user is created without our fixtures.
|
||||||
|
await prisma.user.delete({ where: { email: inviteeEmail } });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can finish team creation", async () => {
|
||||||
|
await page.locator("text=Finish").click();
|
||||||
|
await page.waitForURL("/settings/teams");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can disband team", async () => {
|
||||||
|
await page.locator('[data-testid="team-list-item-link"]').click();
|
||||||
|
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||||
|
await page.locator("text=Disband Team").click();
|
||||||
|
await page.locator("text=Yes, disband team").click();
|
||||||
|
await page.waitForURL("/teams");
|
||||||
|
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
|
||||||
|
const org = await orgs.create({
|
||||||
|
name: "TestOrg",
|
||||||
|
});
|
||||||
|
const teamMatesObj = [
|
||||||
|
{ name: "teammate-1" },
|
||||||
|
{ name: "teammate-2" },
|
||||||
|
{ name: "teammate-3" },
|
||||||
|
{ name: "teammate-4" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const owner = await users.create(
|
||||||
|
{
|
||||||
|
username: "pro-user",
|
||||||
|
name: "pro-user",
|
||||||
|
organizationId: org.id,
|
||||||
|
roleInOrganization: MembershipRole.MEMBER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: teamMatesObj,
|
||||||
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { team } = await owner.getFirstTeam();
|
||||||
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
|
||||||
|
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
||||||
|
await doOnOrgDomain(
|
||||||
|
{
|
||||||
|
orgSlug: org.slug,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await bookTimeSlot(page);
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
// The title of the booking
|
||||||
|
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||||
|
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
|
||||||
|
// The booker should be in the attendee list
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
|
// All the teammates should be in the booking
|
||||||
|
for (const teammate of teamMatesObj) {
|
||||||
|
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Assert whether the user received an email
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
||||||
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
|
const teamMatesObj = [
|
||||||
|
{ name: "teammate-1" },
|
||||||
|
{ name: "teammate-2" },
|
||||||
|
{ name: "teammate-3" },
|
||||||
|
{ name: "teammate-4" },
|
||||||
|
];
|
||||||
|
const owner = await users.create(ownerObj, {
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: teamMatesObj,
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { team } = await owner.getFirstTeam();
|
||||||
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await bookTimeSlot(page);
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
// The person who booked the meeting should be in the attendee list
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
|
// The title of the booking
|
||||||
|
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||||
|
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
|
||||||
|
|
||||||
|
// Since all the users have the same leastRecentlyBooked value
|
||||||
|
// Anyone of the teammates could be the Host of the booking.
|
||||||
|
const chosenUser = await page.getByTestId("booking-host-name").textContent();
|
||||||
|
expect(chosenUser).not.toBeNull();
|
||||||
|
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||||
|
// TODO: Assert whether the user received an email
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doOnOrgDomain(
|
||||||
|
{ orgSlug, page }: { orgSlug: string | null; page: Page },
|
||||||
|
callback: ({ page }: { page: Page }) => Promise<void>
|
||||||
|
) {
|
||||||
|
if (!orgSlug) {
|
||||||
|
throw new Error("orgSlug is not available");
|
||||||
|
}
|
||||||
|
page.setExtraHTTPHeaders({
|
||||||
|
"x-cal-force-slug": orgSlug,
|
||||||
|
});
|
||||||
|
await callback({ page });
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ test.afterAll(async ({ users }) => {
|
|||||||
test.describe("Unpublished", () => {
|
test.describe("Unpublished", () => {
|
||||||
test("Regular team profile", async ({ page, users }) => {
|
test("Regular team profile", async ({ page, users }) => {
|
||||||
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
|
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||||
await page.goto(`/team/${requestedSlug}`);
|
await page.goto(`/team/${requestedSlug}`);
|
||||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||||
@@ -33,7 +33,7 @@ test.describe("Unpublished", () => {
|
|||||||
isUnpublished: true,
|
isUnpublished: true,
|
||||||
schedulingType: SchedulingType.COLLECTIVE,
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
});
|
});
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||||
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
|
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
|
||||||
|
|||||||
@@ -605,7 +605,7 @@
|
|||||||
"hide_book_a_team_member": "Hide Book a Team Member Button",
|
"hide_book_a_team_member": "Hide Book a Team Member Button",
|
||||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Danger zone",
|
||||||
"account_deletion_cannot_be_undone":"Careful. Account deletion cannot be undone.",
|
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cancel_all_remaining": "Cancel all remaining",
|
"cancel_all_remaining": "Cancel all remaining",
|
||||||
@@ -1222,6 +1222,7 @@
|
|||||||
"organizer_name_variable": "Organizer name",
|
"organizer_name_variable": "Organizer name",
|
||||||
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
||||||
"invalid_number": "Invalid phone number",
|
"invalid_number": "Invalid phone number",
|
||||||
|
"invalid_url_error_message": "Invalid URL for {{label}}. Sample URL: {{sampleUrl}}",
|
||||||
"navigate": "Navigate",
|
"navigate": "Navigate",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -1296,6 +1297,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
tracesSampleRate: 0,
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import prismock from "../../../../tests/libs/__mocks__/prisma";
|
|||||||
import { diff } from "jest-diff";
|
import { diff } from "jest-diff";
|
||||||
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
|
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
@@ -874,6 +875,124 @@ describe("getSchedule", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
test("test that booking limit is working correctly if user is all day available", async () => {
|
||||||
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||||
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||||
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
||||||
|
|
||||||
|
const scenarioData = {
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
length: 60,
|
||||||
|
beforeEventBuffer: 0,
|
||||||
|
afterEventBuffer: 0,
|
||||||
|
bookingLimits: {
|
||||||
|
PER_DAY: 1,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
length: 60,
|
||||||
|
beforeEventBuffer: 0,
|
||||||
|
afterEventBuffer: 0,
|
||||||
|
bookingLimits: {
|
||||||
|
PER_DAY: 2,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
...TestData.users.example,
|
||||||
|
id: 101,
|
||||||
|
schedules: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "All Day available",
|
||||||
|
availability: [
|
||||||
|
{
|
||||||
|
userId: null,
|
||||||
|
eventTypeId: null,
|
||||||
|
days: [0, 1, 2, 3, 4, 5, 6],
|
||||||
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
||||||
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
||||||
|
date: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeZone: Timezones["+5:30"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookings: [
|
||||||
|
{
|
||||||
|
userId: 101,
|
||||||
|
eventTypeId: 1,
|
||||||
|
startTime: `${plus2DateString}T08:30:00.000Z`,
|
||||||
|
endTime: `${plus2DateString}T08:29:59.999Z`,
|
||||||
|
status: "ACCEPTED" as BookingStatus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 101,
|
||||||
|
eventTypeId: 2,
|
||||||
|
startTime: `${plus2DateString}T08:30:00.000Z`,
|
||||||
|
endTime: `${plus2DateString}T08:29:59.999Z`,
|
||||||
|
status: "ACCEPTED" as BookingStatus,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await createBookingScenario(scenarioData);
|
||||||
|
|
||||||
|
const thisUserAvailabilityBookingLimitOne = await getSchedule({
|
||||||
|
input: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
eventTypeSlug: "",
|
||||||
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
||||||
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
||||||
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const thisUserAvailabilityBookingLimitTwo = await getSchedule({
|
||||||
|
input: {
|
||||||
|
eventTypeId: 2,
|
||||||
|
eventTypeSlug: "",
|
||||||
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
||||||
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
||||||
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let availableSlotsInTz: dayjs.Dayjs[] = [];
|
||||||
|
for (const date in thisUserAvailabilityBookingLimitOne.slots) {
|
||||||
|
thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => {
|
||||||
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
||||||
|
|
||||||
|
availableSlotsInTz = [];
|
||||||
|
for (const date in thisUserAvailabilityBookingLimitTwo.slots) {
|
||||||
|
thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => {
|
||||||
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Team Event", () => {
|
describe("Team Event", () => {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
|||||||
id: number;
|
id: number;
|
||||||
defaultScheduleId?: number | null;
|
defaultScheduleId?: number | null;
|
||||||
credentials?: InputCredential[];
|
credentials?: InputCredential[];
|
||||||
|
organizationId?: number | null;
|
||||||
selectedCalendars?: InputSelectedCalendar[];
|
selectedCalendars?: InputSelectedCalendar[];
|
||||||
schedules: {
|
schedules: {
|
||||||
// Allows giving id in the input directly so that it can be referenced somewhere else as well
|
// Allows giving id in the input directly so that it can be referenced somewhere else as well
|
||||||
@@ -100,6 +101,9 @@ export type InputEventType = {
|
|||||||
requiresConfirmation?: boolean;
|
requiresConfirmation?: boolean;
|
||||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||||
schedule?: InputUser["schedules"][number];
|
schedule?: InputUser["schedules"][number];
|
||||||
|
bookingLimits?: {
|
||||||
|
PER_DAY?: number;
|
||||||
|
};
|
||||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
||||||
|
|
||||||
type WhiteListedBookingProps = {
|
type WhiteListedBookingProps = {
|
||||||
@@ -198,6 +202,7 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser
|
|||||||
timeZone: null,
|
timeZone: null,
|
||||||
beforeEventBuffer: 0,
|
beforeEventBuffer: 0,
|
||||||
afterEventBuffer: 0,
|
afterEventBuffer: 0,
|
||||||
|
bookingLimits: {},
|
||||||
schedulingType: null,
|
schedulingType: null,
|
||||||
length: 15,
|
length: 15,
|
||||||
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
|
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
|
||||||
@@ -264,8 +269,21 @@ async function addBookingsToDb(
|
|||||||
})[]
|
})[]
|
||||||
) {
|
) {
|
||||||
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
|
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
|
||||||
|
|
||||||
|
function getDateObj(time: string | Date) {
|
||||||
|
return time instanceof Date ? time : new Date(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that we store the date in Date object always. This is to ensure consistency which Prisma does but not prismock
|
||||||
|
log.silly("Handling Prismock bug-3");
|
||||||
|
const fixedBookings = bookings.map((booking) => {
|
||||||
|
const startTime = getDateObj(booking.startTime);
|
||||||
|
const endTime = getDateObj(booking.endTime);
|
||||||
|
return { ...booking, startTime, endTime };
|
||||||
|
});
|
||||||
|
|
||||||
await prismock.booking.createMany({
|
await prismock.booking.createMany({
|
||||||
data: bookings,
|
data: fixedBookings,
|
||||||
});
|
});
|
||||||
log.silly(
|
log.silly(
|
||||||
"TestData: Bookings as in DB",
|
"TestData: Bookings as in DB",
|
||||||
@@ -406,6 +424,7 @@ async function addUsers(users: InputUser[]) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
@@ -446,6 +465,16 @@ export async function createBookingScenario(data: ScenarioData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createOrganization(orgData: { name: string; slug: string }) {
|
||||||
|
const org = await prismock.team.create({
|
||||||
|
data: {
|
||||||
|
name: orgData.name,
|
||||||
|
slug: orgData.slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
|
||||||
// async function addPaymentsToDb(payments: Prisma.PaymentCreateInput[]) {
|
// async function addPaymentsToDb(payments: Prisma.PaymentCreateInput[]) {
|
||||||
// await prismaMock.payment.createMany({
|
// await prismaMock.payment.createMany({
|
||||||
// data: payments,
|
// data: payments,
|
||||||
@@ -722,6 +751,7 @@ export function getOrganizer({
|
|||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
...TestData.users.example,
|
...TestData.users.example,
|
||||||
|
organizationId: null as null | number,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
id,
|
id,
|
||||||
@@ -733,24 +763,33 @@ export function getOrganizer({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScenarioData({
|
export function getScenarioData(
|
||||||
organizer,
|
{
|
||||||
eventTypes,
|
organizer,
|
||||||
usersApartFromOrganizer = [],
|
eventTypes,
|
||||||
apps = [],
|
usersApartFromOrganizer = [],
|
||||||
webhooks,
|
apps = [],
|
||||||
bookings,
|
webhooks,
|
||||||
}: // hosts = [],
|
bookings,
|
||||||
{
|
}: // hosts = [],
|
||||||
organizer: ReturnType<typeof getOrganizer>;
|
{
|
||||||
eventTypes: ScenarioData["eventTypes"];
|
organizer: ReturnType<typeof getOrganizer>;
|
||||||
apps?: ScenarioData["apps"];
|
eventTypes: ScenarioData["eventTypes"];
|
||||||
usersApartFromOrganizer?: ScenarioData["users"];
|
apps?: ScenarioData["apps"];
|
||||||
webhooks?: ScenarioData["webhooks"];
|
usersApartFromOrganizer?: ScenarioData["users"];
|
||||||
bookings?: ScenarioData["bookings"];
|
webhooks?: ScenarioData["webhooks"];
|
||||||
// hosts?: ScenarioData["hosts"];
|
bookings?: ScenarioData["bookings"];
|
||||||
}) {
|
// hosts?: ScenarioData["hosts"];
|
||||||
|
},
|
||||||
|
org?: { id: number | null } | undefined | null
|
||||||
|
) {
|
||||||
const users = [organizer, ...usersApartFromOrganizer];
|
const users = [organizer, ...usersApartFromOrganizer];
|
||||||
|
if (org) {
|
||||||
|
users.forEach((user) => {
|
||||||
|
user.organizationId = org.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
eventTypes.forEach((eventType) => {
|
eventTypes.forEach((eventType) => {
|
||||||
if (
|
if (
|
||||||
eventType.users?.filter((eventTypeUser) => {
|
eventType.users?.filter((eventTypeUser) => {
|
||||||
@@ -897,6 +936,7 @@ export function mockCalendar(
|
|||||||
url: "https://UNUSED_URL",
|
url: "https://UNUSED_URL",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
deleteEvent: async (...rest: any[]) => {
|
deleteEvent: async (...rest: any[]) => {
|
||||||
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
@@ -1021,6 +1061,7 @@ export function mockVideoApp({
|
|||||||
...videoMeetingData,
|
...videoMeetingData,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
deleteMeeting: async (...rest: any[]) => {
|
deleteMeeting: async (...rest: any[]) => {
|
||||||
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
|
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
|
||||||
deleteMeetingCalls.push({
|
deleteMeetingCalls.push({
|
||||||
@@ -1153,7 +1194,6 @@ export async function mockPaymentSuccessWebhookFromStripe({ externalId }: { exte
|
|||||||
await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId }));
|
await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e));
|
log.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e));
|
||||||
|
|
||||||
webhookResponse = e as HttpError;
|
webhookResponse = e as HttpError;
|
||||||
}
|
}
|
||||||
return { webhookResponse };
|
return { webhookResponse };
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
|||||||
|
|
||||||
import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client";
|
import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client";
|
||||||
import { parse } from "node-html-parser";
|
import { parse } from "node-html-parser";
|
||||||
|
import type { VEvent } from "node-ical";
|
||||||
import ical from "node-ical";
|
import ical from "node-ical";
|
||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
import "vitest-fetch-mock";
|
import "vitest-fetch-mock";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { DEFAULT_TIMEZONE_BOOKER } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
@@ -15,42 +19,73 @@ import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
|||||||
|
|
||||||
import type { InputEventType } from "./bookingScenario";
|
import type { InputEventType } from "./bookingScenario";
|
||||||
|
|
||||||
|
// This is too complex at the moment, I really need to simplify this.
|
||||||
|
// Maybe we can replace the exact match with a partial match approach that would be easier to maintain but we would still need Dayjs to do the timezone conversion
|
||||||
|
// Alternative could be that we use some other library to do the timezone conversion?
|
||||||
|
function formatDateToWhenFormat({ start, end }: { start: Date; end: Date }, timeZone: string) {
|
||||||
|
const startTime = dayjs(start).tz(timeZone);
|
||||||
|
return `${startTime.format(`dddd, LL`)} | ${startTime.format("h:mma")} - ${dayjs(end)
|
||||||
|
.tz(timeZone)
|
||||||
|
.format("h:mma")} (${timeZone})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recurrence = {
|
||||||
|
freq: number;
|
||||||
|
interval: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
type ExpectedEmail = {
|
||||||
|
/**
|
||||||
|
* Checks the main heading of the email - Also referred to as title in code at some places
|
||||||
|
*/
|
||||||
|
heading?: string;
|
||||||
|
links?: { text: string; href: string }[];
|
||||||
|
/**
|
||||||
|
* Checks the sub heading of the email - Also referred to as subTitle in code
|
||||||
|
*/
|
||||||
|
subHeading?: string;
|
||||||
|
/**
|
||||||
|
* Checks the <title> tag - Not sure what's the use of it, as it is not shown in UI it seems.
|
||||||
|
*/
|
||||||
|
titleTag?: string;
|
||||||
|
to: string;
|
||||||
|
bookingTimeRange?: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
timeZone: string;
|
||||||
|
};
|
||||||
|
// TODO: Implement these and more
|
||||||
|
// what?: string;
|
||||||
|
// when?: string;
|
||||||
|
// who?: string;
|
||||||
|
// where?: string;
|
||||||
|
// additionalNotes?: string;
|
||||||
|
// footer?: {
|
||||||
|
// rescheduleLink?: string;
|
||||||
|
// cancelLink?: string;
|
||||||
|
// };
|
||||||
|
ics?: {
|
||||||
|
filename: string;
|
||||||
|
iCalUID: string;
|
||||||
|
recurrence?: Recurrence;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Checks that there is no
|
||||||
|
*/
|
||||||
|
noIcs?: true;
|
||||||
|
appsStatus?: AppsStatus[];
|
||||||
|
};
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
toHaveEmail(
|
toHaveEmail(expectedEmail: ExpectedEmail, to: string): R;
|
||||||
expectedEmail: {
|
|
||||||
title?: string;
|
|
||||||
to: string;
|
|
||||||
noIcs?: true;
|
|
||||||
ics?: {
|
|
||||||
filename: string;
|
|
||||||
iCalUID: string;
|
|
||||||
};
|
|
||||||
appsStatus?: AppsStatus[];
|
|
||||||
},
|
|
||||||
to: string
|
|
||||||
): R;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toHaveEmail(
|
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string) {
|
||||||
emails: Fixtures["emails"],
|
|
||||||
expectedEmail: {
|
|
||||||
title?: string;
|
|
||||||
to: string;
|
|
||||||
ics: {
|
|
||||||
filename: string;
|
|
||||||
iCalUID: string;
|
|
||||||
};
|
|
||||||
noIcs: true;
|
|
||||||
appsStatus: AppsStatus[];
|
|
||||||
},
|
|
||||||
to: string
|
|
||||||
) {
|
|
||||||
const { isNot } = this;
|
const { isNot } = this;
|
||||||
const testEmail = emails.get().find((email) => email.to.includes(to));
|
const testEmail = emails.get().find((email) => email.to.includes(to));
|
||||||
const emailsToLog = emails
|
const emailsToLog = emails
|
||||||
@@ -66,6 +101,7 @@ expect.extend({
|
|||||||
}
|
}
|
||||||
const ics = testEmail.icalEvent;
|
const ics = testEmail.icalEvent;
|
||||||
const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null;
|
const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null;
|
||||||
|
const iCalUidData = icsObject ? icsObject[expectedEmail.ics?.iCalUID || ""] : null;
|
||||||
|
|
||||||
let isToAddressExpected = true;
|
let isToAddressExpected = true;
|
||||||
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
|
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
|
||||||
@@ -75,13 +111,18 @@ expect.extend({
|
|||||||
const emailDom = parse(testEmail.html);
|
const emailDom = parse(testEmail.html);
|
||||||
|
|
||||||
const actualEmailContent = {
|
const actualEmailContent = {
|
||||||
title: emailDom.querySelector("title")?.innerText,
|
titleTag: emailDom.querySelector("title")?.innerText,
|
||||||
subject: emailDom.querySelector("subject")?.innerText,
|
heading: emailDom.querySelector('[data-testid="heading"]')?.innerText,
|
||||||
|
subHeading: emailDom.querySelector('[data-testid="subHeading"]')?.innerText,
|
||||||
|
when: emailDom.querySelector('[data-testid="when"]')?.innerText,
|
||||||
|
links: emailDom.querySelectorAll("a[href]").map((link) => ({
|
||||||
|
text: link.innerText,
|
||||||
|
href: link.getAttribute("href"),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedEmailContent = {
|
const expectedEmailContent = getExpectedEmailContent(expectedEmail);
|
||||||
title: expectedEmail.title,
|
assertHasRecurrence(expectedEmail.ics?.recurrence, (iCalUidData as VEvent)?.rrule?.toString() || "");
|
||||||
};
|
|
||||||
|
|
||||||
const isEmailContentMatched = this.equals(
|
const isEmailContentMatched = this.equals(
|
||||||
actualEmailContent,
|
actualEmailContent,
|
||||||
@@ -114,7 +155,7 @@ expect.extend({
|
|||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
actual: ics?.filename,
|
actual: ics?.filename,
|
||||||
expected: expectedEmail.ics.filename,
|
expected: expectedEmail.ics?.filename,
|
||||||
message: () => `ICS Filename ${isNot ? "is" : "is not"} matching`,
|
message: () => `ICS Filename ${isNot ? "is" : "is not"} matching`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -123,11 +164,18 @@ expect.extend({
|
|||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
actual: JSON.stringify(icsObject),
|
actual: JSON.stringify(icsObject),
|
||||||
expected: expectedEmail.ics.iCalUID,
|
expected: expectedEmail.ics?.iCalUID,
|
||||||
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
|
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expectedEmail.noIcs && ics) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `${isNot ? "" : "Not"} expected ics file`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (expectedEmail.appsStatus) {
|
if (expectedEmail.appsStatus) {
|
||||||
const actualAppsStatus = emailDom.querySelectorAll('[data-testid="appsStatus"] li').map((li) => {
|
const actualAppsStatus = emailDom.querySelectorAll('[data-testid="appsStatus"] li').map((li) => {
|
||||||
return li.innerText.trim();
|
return li.innerText.trim();
|
||||||
@@ -155,6 +203,50 @@ expect.extend({
|
|||||||
pass: true,
|
pass: true,
|
||||||
message: () => `Email ${isNot ? "is" : "isn't"} correct`,
|
message: () => `Email ${isNot ? "is" : "isn't"} correct`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getExpectedEmailContent(expectedEmail: ExpectedEmail) {
|
||||||
|
const bookingTimeRange = expectedEmail.bookingTimeRange;
|
||||||
|
const when = bookingTimeRange
|
||||||
|
? formatDateToWhenFormat(
|
||||||
|
{
|
||||||
|
start: bookingTimeRange.start,
|
||||||
|
end: bookingTimeRange.end,
|
||||||
|
},
|
||||||
|
bookingTimeRange.timeZone
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const expectedEmailContent = {
|
||||||
|
titleTag: expectedEmail.titleTag,
|
||||||
|
heading: expectedEmail.heading,
|
||||||
|
subHeading: expectedEmail.subHeading,
|
||||||
|
when: when ? (expectedEmail.ics?.recurrence ? `starting ${when}` : `${when}`) : undefined,
|
||||||
|
links: expect.arrayContaining(expectedEmail.links || []),
|
||||||
|
};
|
||||||
|
// Remove undefined props so that they aren't matched, they are intentionally left undefined because we don't want to match them
|
||||||
|
Object.keys(expectedEmailContent).filter((key) => {
|
||||||
|
if (expectedEmailContent[key as keyof typeof expectedEmailContent] === undefined) {
|
||||||
|
delete expectedEmailContent[key as keyof typeof expectedEmailContent];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return expectedEmailContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertHasRecurrence(expectedRecurrence: Recurrence | null | undefined, rrule: string) {
|
||||||
|
if (!expectedRecurrence) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedRrule = `FREQ=${
|
||||||
|
expectedRecurrence.freq === 0 ? "YEARLY" : expectedRecurrence.freq === 1 ? "MONTHLY" : "WEEKLY"
|
||||||
|
};COUNT=${expectedRecurrence.count};INTERVAL=${expectedRecurrence.interval}`;
|
||||||
|
|
||||||
|
logger.silly({
|
||||||
|
expectedRrule,
|
||||||
|
rrule,
|
||||||
|
});
|
||||||
|
expect(rrule).toContain(expectedRrule);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,21 +327,50 @@ export function expectSuccessfulBookingCreationEmails({
|
|||||||
guests,
|
guests,
|
||||||
otherTeamMembers,
|
otherTeamMembers,
|
||||||
iCalUID,
|
iCalUID,
|
||||||
|
recurrence,
|
||||||
|
bookingTimeRange,
|
||||||
|
booking,
|
||||||
}: {
|
}: {
|
||||||
emails: Fixtures["emails"];
|
emails: Fixtures["emails"];
|
||||||
organizer: { email: string; name: string };
|
organizer: { email: string; name: string; timeZone: string };
|
||||||
booker: { email: string; name: string };
|
booker: { email: string; name: string; timeZone?: string };
|
||||||
guests?: { email: string; name: string }[];
|
guests?: { email: string; name: string; timeZone?: string }[];
|
||||||
otherTeamMembers?: { email: string; name: string }[];
|
otherTeamMembers?: { email: string; name: string; timeZone?: string }[];
|
||||||
iCalUID: string;
|
iCalUID: string;
|
||||||
|
recurrence?: Recurrence;
|
||||||
|
eventDomain?: string;
|
||||||
|
bookingTimeRange?: { start: Date; end: Date };
|
||||||
|
booking: { uid: string; urlOrigin?: string };
|
||||||
}) {
|
}) {
|
||||||
|
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "confirmed_event_type_subject",
|
titleTag: "confirmed_event_type_subject",
|
||||||
|
heading: recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled",
|
||||||
|
subHeading: "",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||||
|
text: "reschedule",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||||
|
text: "cancel",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(bookingTimeRange
|
||||||
|
? {
|
||||||
|
bookingTimeRange: {
|
||||||
|
...bookingTimeRange,
|
||||||
|
timeZone: organizer.timeZone,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
to: `${organizer.email}`,
|
to: `${organizer.email}`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
iCalUID: iCalUID,
|
iCalUID: `${iCalUID}`,
|
||||||
|
recurrence,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
`${organizer.email}`
|
`${organizer.email}`
|
||||||
@@ -257,12 +378,34 @@ export function expectSuccessfulBookingCreationEmails({
|
|||||||
|
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "confirmed_event_type_subject",
|
titleTag: "confirmed_event_type_subject",
|
||||||
|
heading: recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled",
|
||||||
|
subHeading: "emailed_you_and_any_other_attendees",
|
||||||
|
...(bookingTimeRange
|
||||||
|
? {
|
||||||
|
bookingTimeRange: {
|
||||||
|
...bookingTimeRange,
|
||||||
|
// Using the default timezone
|
||||||
|
timeZone: booker.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
to: `${booker.name} <${booker.email}>`,
|
to: `${booker.name} <${booker.email}>`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
iCalUID: iCalUID,
|
iCalUID: iCalUID,
|
||||||
|
recurrence,
|
||||||
},
|
},
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||||
|
text: "reschedule",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||||
|
text: "cancel",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
`${booker.name} <${booker.email}>`
|
`${booker.name} <${booker.email}>`
|
||||||
);
|
);
|
||||||
@@ -271,13 +414,33 @@ export function expectSuccessfulBookingCreationEmails({
|
|||||||
otherTeamMembers.forEach((otherTeamMember) => {
|
otherTeamMembers.forEach((otherTeamMember) => {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "confirmed_event_type_subject",
|
titleTag: "confirmed_event_type_subject",
|
||||||
|
heading: recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled",
|
||||||
|
subHeading: "",
|
||||||
|
...(bookingTimeRange
|
||||||
|
? {
|
||||||
|
bookingTimeRange: {
|
||||||
|
...bookingTimeRange,
|
||||||
|
timeZone: otherTeamMember.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
// Don't know why but organizer and team members of the eventType don'thave their name here like Booker
|
// Don't know why but organizer and team members of the eventType don'thave their name here like Booker
|
||||||
to: `${otherTeamMember.email}`,
|
to: `${otherTeamMember.email}`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
iCalUID: iCalUID,
|
iCalUID: iCalUID,
|
||||||
},
|
},
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||||
|
text: "reschedule",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||||
|
text: "cancel",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
`${otherTeamMember.email}`
|
`${otherTeamMember.email}`
|
||||||
);
|
);
|
||||||
@@ -288,7 +451,17 @@ export function expectSuccessfulBookingCreationEmails({
|
|||||||
guests.forEach((guest) => {
|
guests.forEach((guest) => {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "confirmed_event_type_subject",
|
titleTag: "confirmed_event_type_subject",
|
||||||
|
heading: recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled",
|
||||||
|
subHeading: "emailed_you_and_any_other_attendees",
|
||||||
|
...(bookingTimeRange
|
||||||
|
? {
|
||||||
|
bookingTimeRange: {
|
||||||
|
...bookingTimeRange,
|
||||||
|
timeZone: guest.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
to: `${guest.email}`,
|
to: `${guest.email}`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
@@ -311,7 +484,7 @@ export function expectBrokenIntegrationEmails({
|
|||||||
// Broken Integration email is only sent to the Organizer
|
// Broken Integration email is only sent to the Organizer
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "broken_integration",
|
titleTag: "broken_integration",
|
||||||
to: `${organizer.email}`,
|
to: `${organizer.email}`,
|
||||||
// No ics goes in case of broken integration email it seems
|
// No ics goes in case of broken integration email it seems
|
||||||
// ics: {
|
// ics: {
|
||||||
@@ -344,7 +517,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||||||
}) {
|
}) {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "broken_integration",
|
titleTag: "broken_integration",
|
||||||
to: `${organizer.email}`,
|
to: `${organizer.email}`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
@@ -356,7 +529,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||||||
|
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "calendar_event_creation_failure_subject",
|
titleTag: "calendar_event_creation_failure_subject",
|
||||||
to: `${booker.name} <${booker.email}>`,
|
to: `${booker.name} <${booker.email}>`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
@@ -378,11 +551,11 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||||||
organizer: { email: string; name: string };
|
organizer: { email: string; name: string };
|
||||||
booker: { email: string; name: string };
|
booker: { email: string; name: string };
|
||||||
iCalUID: string;
|
iCalUID: string;
|
||||||
appsStatus: AppsStatus[];
|
appsStatus?: AppsStatus[];
|
||||||
}) {
|
}) {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "event_type_has_been_rescheduled_on_time_date",
|
titleTag: "event_type_has_been_rescheduled_on_time_date",
|
||||||
to: `${organizer.email}`,
|
to: `${organizer.email}`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
@@ -395,7 +568,7 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||||||
|
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "event_type_has_been_rescheduled_on_time_date",
|
titleTag: "event_type_has_been_rescheduled_on_time_date",
|
||||||
to: `${booker.name} <${booker.email}>`,
|
to: `${booker.name} <${booker.email}>`,
|
||||||
ics: {
|
ics: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
@@ -415,7 +588,7 @@ export function expectAwaitingPaymentEmails({
|
|||||||
}) {
|
}) {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "awaiting_payment_subject",
|
titleTag: "awaiting_payment_subject",
|
||||||
to: `${booker.name} <${booker.email}>`,
|
to: `${booker.name} <${booker.email}>`,
|
||||||
noIcs: true,
|
noIcs: true,
|
||||||
},
|
},
|
||||||
@@ -434,7 +607,7 @@ export function expectBookingRequestedEmails({
|
|||||||
}) {
|
}) {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "event_awaiting_approval_subject",
|
titleTag: "event_awaiting_approval_subject",
|
||||||
to: `${organizer.email}`,
|
to: `${organizer.email}`,
|
||||||
noIcs: true,
|
noIcs: true,
|
||||||
},
|
},
|
||||||
@@ -443,7 +616,7 @@ export function expectBookingRequestedEmails({
|
|||||||
|
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
title: "booking_submitted_subject",
|
titleTag: "booking_submitted_subject",
|
||||||
to: `${booker.email}`,
|
to: `${booker.email}`,
|
||||||
noIcs: true,
|
noIcs: true,
|
||||||
},
|
},
|
||||||
@@ -629,32 +802,42 @@ export function expectSuccessfulCalendarEventCreationInCalendar(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
updateEventCalls: any[];
|
updateEventCalls: any[];
|
||||||
},
|
},
|
||||||
expected: {
|
expected:
|
||||||
calendarId?: string | null;
|
| {
|
||||||
videoCallUrl: string;
|
calendarId?: string | null;
|
||||||
destinationCalendars: Partial<DestinationCalendar>[];
|
videoCallUrl: string;
|
||||||
}
|
destinationCalendars?: Partial<DestinationCalendar>[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
calendarId?: string | null;
|
||||||
|
videoCallUrl: string;
|
||||||
|
destinationCalendars?: Partial<DestinationCalendar>[];
|
||||||
|
}[]
|
||||||
) {
|
) {
|
||||||
expect(calendarMock.createEventCalls.length).toBe(1);
|
const expecteds = expected instanceof Array ? expected : [expected];
|
||||||
const call = calendarMock.createEventCalls[0];
|
expect(calendarMock.createEventCalls.length).toBe(expecteds.length);
|
||||||
const calEvent = call[0];
|
for (let i = 0; i < calendarMock.createEventCalls.length; i++) {
|
||||||
|
const expected = expecteds[i];
|
||||||
|
|
||||||
expect(calEvent).toEqual(
|
const calEvent = calendarMock.createEventCalls[i][0];
|
||||||
expect.objectContaining({
|
|
||||||
destinationCalendar: expected.calendarId
|
expect(calEvent).toEqual(
|
||||||
? [
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
destinationCalendar: expected.calendarId
|
||||||
externalId: expected.calendarId,
|
? [
|
||||||
}),
|
expect.objectContaining({
|
||||||
]
|
externalId: expected.calendarId,
|
||||||
: expected.destinationCalendars
|
}),
|
||||||
? expect.arrayContaining(expected.destinationCalendars.map((cal) => expect.objectContaining(cal)))
|
]
|
||||||
: null,
|
: expected.destinationCalendars
|
||||||
videoCallData: expect.objectContaining({
|
? expect.arrayContaining(expected.destinationCalendars.map((cal) => expect.objectContaining(cal)))
|
||||||
url: expected.videoCallUrl,
|
: null,
|
||||||
}),
|
videoCallData: expect.objectContaining({
|
||||||
})
|
url: expected.videoCallUrl,
|
||||||
);
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectSuccessfulCalendarEventUpdationInCalendar(
|
export function expectSuccessfulCalendarEventUpdationInCalendar(
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfig } from "checkly";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://www.checklyhq.com/docs/cli/project-structure/
|
||||||
|
*/
|
||||||
|
const config = defineConfig({
|
||||||
|
/* A human friendly name for your project */
|
||||||
|
projectName: "calcom-monorepo",
|
||||||
|
/** A logical ID that needs to be unique across your Checkly account,
|
||||||
|
* See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs.
|
||||||
|
*/
|
||||||
|
logicalId: "calcom-monorepo",
|
||||||
|
/* An optional URL to your Git repo */
|
||||||
|
repoUrl: "https://github.com/checkly/checkly-cli",
|
||||||
|
/* Sets default values for Checks */
|
||||||
|
checks: {
|
||||||
|
/* A default for how often your Check should run in minutes */
|
||||||
|
frequency: 10,
|
||||||
|
/* Checkly data centers to run your Checks as monitors */
|
||||||
|
locations: ["us-east-1", "eu-west-1"],
|
||||||
|
/* An optional array of tags to organize your Checks */
|
||||||
|
tags: ["Web"],
|
||||||
|
/** The Checkly Runtime identifier, determining npm packages and the Node.js version available at runtime.
|
||||||
|
* See https://www.checklyhq.com/docs/cli/npm-packages/
|
||||||
|
*/
|
||||||
|
runtimeId: "2023.02",
|
||||||
|
/* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */
|
||||||
|
checkMatch: "**/__checks__/**/*.check.ts",
|
||||||
|
browserChecks: {
|
||||||
|
/* A glob pattern matches any Playwright .spec.ts files and automagically creates a Browser Check. This way, you
|
||||||
|
* can just write native Playwright code. See https://www.checklyhq.com/docs/cli/using-check-test-match/
|
||||||
|
* */
|
||||||
|
testMatch: "**/__checks__/**/*.spec.ts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cli: {
|
||||||
|
/* The default datacenter location to use when running npx checkly test */
|
||||||
|
runLocation: "eu-west-1",
|
||||||
|
/* An array of default reporters to use when a reporter is not specified with the "--reporter" flag */
|
||||||
|
reporters: ["list"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## Infrastructure Folder
|
||||||
|
This folder, located within the "infra" directory of our monorepo, is dedicated to managing the infrastructure as code (IaC) and Docker-related files for our project. It plays a critical role in orchestrating, configuring, and maintaining the infrastructure that our applications rely on.
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# syntax = docker/dockerfile:1
|
||||||
|
|
||||||
|
# Adjust NODE_VERSION as desired
|
||||||
|
ARG NODE_VERSION=20.7.0
|
||||||
|
FROM node:${NODE_VERSION}-slim as base
|
||||||
|
|
||||||
|
# Next.js/Prisma app lives here
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set production environment
|
||||||
|
ENV NODE_ENV="production"
|
||||||
|
|
||||||
|
|
||||||
|
# Throw-away build stage to reduce size of final image
|
||||||
|
FROM base as build
|
||||||
|
|
||||||
|
# copy all required files from the monorepo
|
||||||
|
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json git-init.sh git-setup.sh ./
|
||||||
|
COPY /.yarn ./.yarn
|
||||||
|
COPY ./apps/api ./apps/api
|
||||||
|
COPY ./packages ./packages
|
||||||
|
# TODO: follow up pr to remove dependencies on web
|
||||||
|
COPY ./apps/web ./apps/web
|
||||||
|
|
||||||
|
# Install node modules and dependencies, prune unneeded deps, then build
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update -qq && \
|
||||||
|
apt-get install -y build-essential openssl pkg-config python-is-python3 && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||||
|
yarn config set httpTimeout 1200000 && \
|
||||||
|
npx turbo prune --scope=@calcom/web --docker && \
|
||||||
|
npx turbo prune --scope=@calcom/api --docker && \
|
||||||
|
yarn install && \
|
||||||
|
yarn turbo run build --filter=@calcom/api
|
||||||
|
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install packages needed for deployment
|
||||||
|
RUN apt-get update -qq && \
|
||||||
|
apt-get install --no-install-recommends -y openssl && \
|
||||||
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Copy built application
|
||||||
|
COPY --from=build /app/package.json ./package.json
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
|
||||||
|
COPY --from=build /app/apps/api/.next ./apps/api/.next
|
||||||
|
COPY --from=build /app/apps/api/.turbo ./apps/api/.turbo
|
||||||
|
COPY --from=build /app/turbo.json ./turbo.json
|
||||||
|
COPY --from=build /app/yarn.lock ./yarn.lock
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start cmd, called when docker image is mounted
|
||||||
|
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
|
||||||
+5
-1
@@ -71,7 +71,10 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"type-check": "turbo run type-check",
|
"type-check": "turbo run type-check",
|
||||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||||
"web": "yarn workspace @calcom/web"
|
"web": "yarn workspace @calcom/web",
|
||||||
|
"docker-build-api": "docker build -t cal-api -f ./infra/docker/api/Dockerfile .",
|
||||||
|
"docker-run-api": "docker run -p 80:80 cal-api",
|
||||||
|
"docker-stop-api": "docker ps --filter 'ancestor=cal-api' -q | xargs docker stop"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.26.1",
|
"@changesets/cli": "^2.26.1",
|
||||||
@@ -81,6 +84,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@types/jsonwebtoken": "^9.0.3",
|
"@types/jsonwebtoken": "^9.0.3",
|
||||||
"c8": "^7.13.0",
|
"c8": "^7.13.0",
|
||||||
|
"checkly": "latest",
|
||||||
"dotenv-checker": "^1.1.5",
|
"dotenv-checker": "^1.1.5",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"i18n-unused": "^0.13.0",
|
"i18n-unused": "^0.13.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { calendar_v3 } from "googleapis";
|
|||||||
import { google } from "googleapis";
|
import { google } from "googleapis";
|
||||||
|
|
||||||
import { MeetLocationType } from "@calcom/app-store/locations";
|
import { MeetLocationType } from "@calcom/app-store/locations";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
import type CalendarService from "@calcom/lib/CalendarService";
|
import type CalendarService from "@calcom/lib/CalendarService";
|
||||||
@@ -369,57 +370,75 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
timeMin: string;
|
timeMin: string;
|
||||||
timeMax: string;
|
timeMax: string;
|
||||||
items: { id: string }[];
|
items: { id: string }[];
|
||||||
}): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
}): Promise<EventBusyDate[] | null> {
|
||||||
const calendar = await this.authedCalendar();
|
const calendar = await this.authedCalendar();
|
||||||
const flags = await getFeatureFlagMap(prisma);
|
const flags = await getFeatureFlagMap(prisma);
|
||||||
|
|
||||||
|
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {};
|
||||||
if (!flags["calendar-cache"]) {
|
if (!flags["calendar-cache"]) {
|
||||||
this.log.warn("Calendar Cache is disabled - Skipping");
|
this.log.warn("Calendar Cache is disabled - Skipping");
|
||||||
const { timeMin, timeMax, items } = args;
|
const { timeMin, timeMax, items } = args;
|
||||||
const apires = await calendar.freebusy.query({
|
const apires = await calendar.freebusy.query({
|
||||||
requestBody: { timeMin, timeMax, items },
|
requestBody: { timeMin, timeMax, items },
|
||||||
});
|
});
|
||||||
return apires.data;
|
|
||||||
|
freeBusyResult = apires.data;
|
||||||
|
} else {
|
||||||
|
const { timeMin: _timeMin, timeMax: _timeMax, items } = args;
|
||||||
|
const { timeMin, timeMax } = handleMinMax(_timeMin, _timeMax);
|
||||||
|
const key = JSON.stringify({ timeMin, timeMax, items });
|
||||||
|
const cached = await prisma.calendarCache.findUnique({
|
||||||
|
where: {
|
||||||
|
credentialId_key: {
|
||||||
|
credentialId: this.credential.id,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
expiresAt: { gte: new Date(Date.now()) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
freeBusyResult = cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||||
|
} else {
|
||||||
|
const apires = await calendar.freebusy.query({
|
||||||
|
requestBody: { timeMin, timeMax, items },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skipping await to respond faster
|
||||||
|
await prisma.calendarCache.upsert({
|
||||||
|
where: {
|
||||||
|
credentialId_key: {
|
||||||
|
credentialId: this.credential.id,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
value: JSON.parse(JSON.stringify(apires.data)),
|
||||||
|
expiresAt: new Date(Date.now() + CACHING_TIME),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
value: JSON.parse(JSON.stringify(apires.data)),
|
||||||
|
credentialId: this.credential.id,
|
||||||
|
key,
|
||||||
|
expiresAt: new Date(Date.now() + CACHING_TIME),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
freeBusyResult = apires.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const { timeMin: _timeMin, timeMax: _timeMax, items } = args;
|
if (!freeBusyResult.calendars) return null;
|
||||||
const { timeMin, timeMax } = handleMinMax(_timeMin, _timeMax);
|
|
||||||
const key = JSON.stringify({ timeMin, timeMax, items });
|
|
||||||
const cached = await prisma.calendarCache.findUnique({
|
|
||||||
where: {
|
|
||||||
credentialId_key: {
|
|
||||||
credentialId: this.credential.id,
|
|
||||||
key,
|
|
||||||
},
|
|
||||||
expiresAt: { gte: new Date(Date.now()) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
const result = Object.values(freeBusyResult.calendars).reduce((c, i) => {
|
||||||
|
i.busy?.forEach((busyTime) => {
|
||||||
const apires = await calendar.freebusy.query({
|
c.push({
|
||||||
requestBody: { timeMin, timeMax, items },
|
start: busyTime.start || "",
|
||||||
});
|
end: busyTime.end || "",
|
||||||
|
});
|
||||||
// Skipping await to respond faster
|
});
|
||||||
await prisma.calendarCache.upsert({
|
return c;
|
||||||
where: {
|
}, [] as Prisma.PromiseReturnType<CalendarService["getAvailability"]>);
|
||||||
credentialId_key: {
|
return result;
|
||||||
credentialId: this.credential.id,
|
|
||||||
key,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
value: JSON.parse(JSON.stringify(apires.data)),
|
|
||||||
expiresAt: new Date(Date.now() + CACHING_TIME),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
value: JSON.parse(JSON.stringify(apires.data)),
|
|
||||||
credentialId: this.credential.id,
|
|
||||||
key,
|
|
||||||
expiresAt: new Date(Date.now() + CACHING_TIME),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return apires.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailability(
|
async getAvailability(
|
||||||
@@ -444,22 +463,44 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const calsIds = await getCalIds();
|
const calsIds = await getCalIds();
|
||||||
const freeBusyData = await this.getCacheOrFetchAvailability({
|
const originalStartDate = dayjs(dateFrom);
|
||||||
timeMin: dateFrom,
|
const originalEndDate = dayjs(dateTo);
|
||||||
timeMax: dateTo,
|
const diff = originalEndDate.diff(originalStartDate, "days");
|
||||||
items: calsIds.map((id) => ({ id })),
|
|
||||||
});
|
// /freebusy from google api only allows a date range of 90 days
|
||||||
if (!freeBusyData?.calendars) throw new Error("No response from google calendar");
|
if (diff <= 90) {
|
||||||
const result = Object.values(freeBusyData.calendars).reduce((c, i) => {
|
const freeBusyData = await this.getCacheOrFetchAvailability({
|
||||||
i.busy?.forEach((busyTime) => {
|
timeMin: dateFrom,
|
||||||
c.push({
|
timeMax: dateTo,
|
||||||
start: busyTime.start || "",
|
items: calsIds.map((id) => ({ id })),
|
||||||
end: busyTime.end || "",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return c;
|
if (!freeBusyData) throw new Error("No response from google calendar");
|
||||||
}, [] as Prisma.PromiseReturnType<CalendarService["getAvailability"]>);
|
|
||||||
return result;
|
return freeBusyData;
|
||||||
|
} else {
|
||||||
|
const busyData = [];
|
||||||
|
|
||||||
|
const loopsNumber = Math.ceil(diff / 90);
|
||||||
|
|
||||||
|
let startDate = originalStartDate;
|
||||||
|
let endDate = originalStartDate.add(90, "days");
|
||||||
|
|
||||||
|
for (let i = 0; i < loopsNumber; i++) {
|
||||||
|
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;
|
||||||
|
|
||||||
|
busyData.push(
|
||||||
|
...((await this.getCacheOrFetchAvailability({
|
||||||
|
timeMin: startDate.format(),
|
||||||
|
timeMax: endDate.format(),
|
||||||
|
items: calsIds.map((id) => ({ id })),
|
||||||
|
})) || [])
|
||||||
|
);
|
||||||
|
|
||||||
|
startDate = endDate.add(1, "minutes");
|
||||||
|
endDate = startDate.add(90, "days");
|
||||||
|
}
|
||||||
|
return busyData;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error("There was an error contacting google calendar service: ", error);
|
this.log.error("There was an error contacting google calendar service: ", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
const isEmbed = params.appPages[1] === "embed";
|
const isEmbed = params.appPages[1] === "embed";
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ export async function getLocationGroupedOptions(
|
|||||||
teamName: credential.team?.name,
|
teamName: credential.team?.name,
|
||||||
}))) {
|
}))) {
|
||||||
const label = `${app.locationOption.label} ${teamName ? `(${teamName})` : ""}`;
|
const label = `${app.locationOption.label} ${teamName ? `(${teamName})` : ""}`;
|
||||||
const option = { ...app.locationOption, label, icon: app.logo, slug: app.slug };
|
const option = {
|
||||||
|
...app.locationOption,
|
||||||
|
label,
|
||||||
|
icon: app.logo,
|
||||||
|
slug: app.slug,
|
||||||
|
...(app.credential ? { credentialId: app.credential.id, teamName: app.credential.team?.name } : {}),
|
||||||
|
};
|
||||||
if (apps[groupByCategory]) {
|
if (apps[groupByCategory]) {
|
||||||
apps[groupByCategory] = [...apps[groupByCategory], option];
|
apps[groupByCategory] = [...apps[groupByCategory], option];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapt
|
|||||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||||
|
import metadata from "../_metadata";
|
||||||
import { getZoomAppKeys } from "./getZoomAppKeys";
|
import { getZoomAppKeys } from "./getZoomAppKeys";
|
||||||
|
|
||||||
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
|
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
|
||||||
@@ -91,7 +92,7 @@ const zoomAuth = (credential: CredentialPayload) => {
|
|||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
"zoom",
|
metadata.slug,
|
||||||
credential.userId
|
credential.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -489,6 +489,22 @@ export default class EventManager {
|
|||||||
*/
|
*/
|
||||||
private async createAllCalendarEvents(event: CalendarEvent) {
|
private async createAllCalendarEvents(event: CalendarEvent) {
|
||||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||||
|
|
||||||
|
const fallbackToFirstConnectedCalendar = async () => {
|
||||||
|
/**
|
||||||
|
* Not ideal but, if we don't find a destination calendar,
|
||||||
|
* fallback to the first connected calendar - Shouldn't be a CRM calendar
|
||||||
|
*/
|
||||||
|
const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar"));
|
||||||
|
if (credential) {
|
||||||
|
const createdEvent = await createEvent(credential, event);
|
||||||
|
log.silly("Created Calendar event", safeStringify({ createdEvent }));
|
||||||
|
if (createdEvent) {
|
||||||
|
createdEvents.push(createdEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||||
// Since GCal pushes events to multiple calendars we only want to create one event per booking
|
// Since GCal pushes events to multiple calendars we only want to create one event per booking
|
||||||
let gCalAdded = false;
|
let gCalAdded = false;
|
||||||
@@ -545,6 +561,14 @@ export default class EventManager {
|
|||||||
);
|
);
|
||||||
// It might not be the first connected calendar as it seems that the order is not guaranteed to be ascending of credentialId.
|
// It might not be the first connected calendar as it seems that the order is not guaranteed to be ascending of credentialId.
|
||||||
const firstCalendarCredential = destinationCalendarCredentials[0];
|
const firstCalendarCredential = destinationCalendarCredentials[0];
|
||||||
|
|
||||||
|
if (!firstCalendarCredential) {
|
||||||
|
log.warn(
|
||||||
|
"No other credentials found of the same type as the destination calendar. Falling back to first connected calendar"
|
||||||
|
);
|
||||||
|
await fallbackToFirstConnectedCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
log.warn(
|
log.warn(
|
||||||
"No credentialId found for destination calendar, falling back to first found calendar",
|
"No credentialId found for destination calendar, falling back to first found calendar",
|
||||||
safeStringify({
|
safeStringify({
|
||||||
@@ -563,19 +587,7 @@ export default class EventManager {
|
|||||||
calendarCredentials: this.calendarCredentials,
|
calendarCredentials: this.calendarCredentials,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
await fallbackToFirstConnectedCalendar();
|
||||||
/**
|
|
||||||
* Not ideal but, if we don't find a destination calendar,
|
|
||||||
* fallback to the first connected calendar - Shouldn't be a CRM calendar
|
|
||||||
*/
|
|
||||||
const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar"));
|
|
||||||
if (credential) {
|
|
||||||
const createdEvent = await createEvent(credential, event);
|
|
||||||
log.silly("Created Calendar event", safeStringify({ createdEvent }));
|
|
||||||
if (createdEvent) {
|
|
||||||
createdEvents.push(createdEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taking care of non-traditional calendar integrations
|
// Taking care of non-traditional calendar integrations
|
||||||
|
|||||||
@@ -255,7 +255,10 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
|
|||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
queryParams.set("rescheduleUid", `${booking.uid}`);
|
queryParams.set("rescheduleUid", `${booking.uid}`);
|
||||||
slug = `${slug}`;
|
slug = `${slug}`;
|
||||||
const rescheduleLink = `${WEBAPP_URL}/${slug}?${queryParams.toString()}`;
|
|
||||||
|
const rescheduleLink = `${
|
||||||
|
this.calendarEvent.bookerUrl ?? WEBAPP_URL
|
||||||
|
}/${slug}?${queryParams.toString()}`;
|
||||||
this.rescheduleLink = rescheduleLink;
|
this.rescheduleLink = rescheduleLink;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
} from "@calcom/types/Calendar";
|
} from "@calcom/types/Calendar";
|
||||||
|
|
||||||
class CalendarEventClass implements CalendarEvent {
|
class CalendarEventClass implements CalendarEvent {
|
||||||
|
bookerUrl?: string | undefined;
|
||||||
type!: string;
|
type!: string;
|
||||||
title!: string;
|
title!: string;
|
||||||
startTime!: string;
|
startTime!: string;
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
|
||||||
import { intersect } from "@calcom/lib/date-ranges";
|
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export const getAggregatedAvailability = (
|
|
||||||
userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[],
|
|
||||||
schedulingType: SchedulingType | null
|
|
||||||
): DateRange[] => {
|
|
||||||
const fixedHosts = userAvailability.filter(
|
|
||||||
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
|
|
||||||
);
|
|
||||||
|
|
||||||
const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges);
|
|
||||||
|
|
||||||
const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
|
|
||||||
if (unfixedHosts.length) {
|
|
||||||
dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges));
|
|
||||||
}
|
|
||||||
|
|
||||||
const availability = intersect(dateRangesToIntersect);
|
|
||||||
|
|
||||||
return mergeOverlappingDateRanges(availability);
|
|
||||||
};
|
|
||||||
|
|
||||||
function isSameDay(date1: Date, date2: Date) {
|
|
||||||
return (
|
|
||||||
date1.getUTCFullYear() === date2.getUTCFullYear() &&
|
|
||||||
date1.getUTCMonth() === date2.getUTCMonth() &&
|
|
||||||
date1.getUTCDate() === date2.getUTCDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
|
||||||
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
|
||||||
|
|
||||||
const mergedDateRanges: DateRange[] = [];
|
|
||||||
|
|
||||||
let currentRange = dateRanges[0];
|
|
||||||
if (!currentRange) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i < dateRanges.length; i++) {
|
|
||||||
const nextRange = dateRanges[i];
|
|
||||||
if (
|
|
||||||
isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) &&
|
|
||||||
currentRange.end.valueOf() > nextRange.start.valueOf()
|
|
||||||
) {
|
|
||||||
currentRange = {
|
|
||||||
start: currentRange.start,
|
|
||||||
end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
mergedDateRanges.push(currentRange);
|
|
||||||
currentRange = nextRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mergedDateRanges.push(currentRange);
|
|
||||||
|
|
||||||
return mergedDateRanges;
|
|
||||||
}
|
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||||
|
|
||||||
|
export function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
||||||
|
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||||
|
|
||||||
|
const mergedDateRanges: DateRange[] = [];
|
||||||
|
|
||||||
|
let currentRange = dateRanges[0];
|
||||||
|
if (!currentRange) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < dateRanges.length; i++) {
|
||||||
|
const nextRange = dateRanges[i];
|
||||||
|
|
||||||
|
if (isCurrentRangeOverlappingNext(currentRange, nextRange)) {
|
||||||
|
currentRange = {
|
||||||
|
start: currentRange.start,
|
||||||
|
end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
mergedDateRanges.push(currentRange);
|
||||||
|
currentRange = nextRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mergedDateRanges.push(currentRange);
|
||||||
|
|
||||||
|
return mergedDateRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentRangeOverlappingNext(currentRange: DateRange, nextRange: DateRange): boolean {
|
||||||
|
return (
|
||||||
|
currentRange.start.valueOf() <= nextRange.start.valueOf() &&
|
||||||
|
currentRange.end.valueOf() > nextRange.start.valueOf()
|
||||||
|
);
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||||
|
|
||||||
|
import { mergeOverlappingDateRanges } from ".";
|
||||||
|
|
||||||
|
const november2 = "2023-11-02";
|
||||||
|
const november3 = "2023-11-03";
|
||||||
|
|
||||||
|
describe("mergeOverlappingDateRanges", () => {
|
||||||
|
it("should merge all ranges into one when one range includes all others", () => {
|
||||||
|
const dateRanges = [
|
||||||
|
createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`), // Includes all others
|
||||||
|
createDateRange(`${november2}T23:15:00.000Z`, `${november3}T00:00:00.000Z`),
|
||||||
|
createDateRange(`${november3}T00:15:00.000Z`, `${november3}T01:00:00.000Z`),
|
||||||
|
createDateRange(`${november3}T01:15:00.000Z`, `${november3}T02:00:00.000Z`),
|
||||||
|
];
|
||||||
|
|
||||||
|
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||||
|
expect(mergedRanges).toHaveLength(1);
|
||||||
|
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||||
|
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge only overlapping ranges over 2 days and leave non-overlapping ranges as is", () => {
|
||||||
|
const dateRanges = [
|
||||||
|
createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`),
|
||||||
|
createDateRange(`${november3}T05:00:00.000Z`, `${november3}T06:00:00.000Z`),
|
||||||
|
createDateRange(`${november3}T08:00:00.000Z`, `${november3}T10:00:00.000Z`), // This range should not be merged
|
||||||
|
];
|
||||||
|
|
||||||
|
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||||
|
expect(mergedRanges).toHaveLength(2);
|
||||||
|
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||||
|
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||||
|
expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true);
|
||||||
|
expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge ranges that overlap on the same day", () => {
|
||||||
|
const dateRanges = [
|
||||||
|
createDateRange(`${november2}T01:00:00.000Z`, `${november2}T04:00:00.000Z`),
|
||||||
|
createDateRange(`${november2}T02:00:00.000Z`, `${november2}T03:00:00.000Z`), // This overlaps with the first range
|
||||||
|
createDateRange(`${november2}T05:00:00.000Z`, `${november2}T06:00:00.000Z`), // This doesn't overlap with above
|
||||||
|
];
|
||||||
|
|
||||||
|
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||||
|
expect(mergedRanges).toHaveLength(2);
|
||||||
|
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||||
|
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||||
|
expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true);
|
||||||
|
expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createDateRange(start: string, end: string): DateRange {
|
||||||
|
return {
|
||||||
|
start: dayjs(start),
|
||||||
|
end: dayjs(end),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||||
|
import { intersect } from "@calcom/lib/date-ranges";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
|
||||||
|
|
||||||
|
export const getAggregatedAvailability = (
|
||||||
|
userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[],
|
||||||
|
schedulingType: SchedulingType | null
|
||||||
|
): DateRange[] => {
|
||||||
|
const fixedHosts = userAvailability.filter(
|
||||||
|
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges);
|
||||||
|
|
||||||
|
const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
|
||||||
|
if (unfixedHosts.length) {
|
||||||
|
dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges));
|
||||||
|
}
|
||||||
|
|
||||||
|
const availability = intersect(dateRangesToIntersect);
|
||||||
|
|
||||||
|
return mergeOverlappingDateRanges(availability);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
import EmailCommonDivider from "./EmailCommonDivider";
|
import EmailCommonDivider from "./EmailCommonDivider";
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ const EmailScheduledBodyHeaderContent = (props: {
|
|||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}>
|
}}>
|
||||||
<div
|
<div
|
||||||
|
data-testid="heading"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
@@ -35,6 +36,7 @@ const EmailScheduledBodyHeaderContent = (props: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="center" style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
<td align="center" style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||||
<div
|
<div
|
||||||
|
data-testid="subHeading"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export function WhenInfo(props: {
|
|||||||
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
|
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<>
|
<span data-testid="when">
|
||||||
{recurringEvent?.count ? `${t("starting")} ` : ""}
|
{recurringEvent?.count ? `${t("starting")} ` : ""}
|
||||||
{getRecipientStart(`dddd, LL | ${timeFormat}`)} - {getRecipientEnd(timeFormat)}{" "}
|
{getRecipientStart(`dddd, LL | ${timeFormat}`)} - {getRecipientEnd(timeFormat)}{" "}
|
||||||
<span style={{ color: "#4B5563" }}>({timeZone})</span>
|
<span style={{ color: "#4B5563" }}>({timeZone})</span>
|
||||||
</>
|
</span>
|
||||||
}
|
}
|
||||||
withSpacer
|
withSpacer
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ const EmailStep = (props: { translationString: string; iconsrc: string }) => {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: "#E5E7EB",
|
backgroundColor: "#E5E7EB",
|
||||||
borderRadius: "48px",
|
borderRadius: "48px",
|
||||||
height: "48px",
|
minHeight: "48px",
|
||||||
width: "48px",
|
minWidth: "48px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -50,8 +50,13 @@ 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(
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export class Cal {
|
|||||||
}: {
|
}: {
|
||||||
calLink: string;
|
calLink: string;
|
||||||
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||||
calOrigin?: string;
|
calOrigin: string | null;
|
||||||
}) {
|
}) {
|
||||||
const iframe = (this.iframe = document.createElement("iframe"));
|
const iframe = (this.iframe = document.createElement("iframe"));
|
||||||
iframe.className = "cal-embed";
|
iframe.className = "cal-embed";
|
||||||
@@ -473,10 +473,12 @@ class CalApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.embedType = "inline";
|
config.embedType = "inline";
|
||||||
|
const calConfig = this.cal.getConfig();
|
||||||
|
|
||||||
const iframe = this.cal.createIframe({
|
const iframe = this.cal.createIframe({
|
||||||
calLink,
|
calLink,
|
||||||
queryObject: withColorScheme(Cal.getQueryObject(config), containerEl),
|
queryObject: withColorScheme(Cal.getQueryObject(config), containerEl),
|
||||||
|
calOrigin: calConfig.calOrigin,
|
||||||
});
|
});
|
||||||
|
|
||||||
iframe.style.height = "100%";
|
iframe.style.height = "100%";
|
||||||
@@ -555,6 +557,7 @@ class CalApi {
|
|||||||
modal({
|
modal({
|
||||||
calLink,
|
calLink,
|
||||||
config = {},
|
config = {},
|
||||||
|
calOrigin,
|
||||||
__prerender = false,
|
__prerender = false,
|
||||||
}: {
|
}: {
|
||||||
calLink: string;
|
calLink: string;
|
||||||
@@ -607,6 +610,7 @@ class CalApi {
|
|||||||
iframe = this.cal.createIframe({
|
iframe = this.cal.createIframe({
|
||||||
calLink,
|
calLink,
|
||||||
queryObject,
|
queryObject,
|
||||||
|
calOrigin: calOrigin || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ export const BookEventFormChild = ({
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setVerifiedEmail(email);
|
setVerifiedEmail(email);
|
||||||
setEmailVerificationModalVisible(false);
|
setEmailVerificationModalVisible(false);
|
||||||
|
bookEvent(bookingForm.getValues());
|
||||||
}}
|
}}
|
||||||
isUserSessionRequiredToVerify={false}
|
isUserSessionRequiredToVerify={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -129,10 +129,6 @@ async function handler(req: CustomRequest) {
|
|||||||
throw new HttpError({ statusCode: 400, message: "Booking not found" });
|
throw new HttpError({ statusCode: 400, message: "Booking not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId !== bookingToDelete.user?.id && bookingToDelete.startTime < new Date()) {
|
|
||||||
throw new HttpError({ statusCode: 400, message: "Cannot cancel past events" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bookingToDelete.userId) {
|
if (!bookingToDelete.userId) {
|
||||||
throw new HttpError({ statusCode: 400, message: "User not found" });
|
throw new HttpError({ statusCode: 400, message: "User not found" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,22 +408,7 @@ async function ensureAvailableUsers(
|
|||||||
|
|
||||||
let foundConflict = false;
|
let foundConflict = false;
|
||||||
try {
|
try {
|
||||||
if (
|
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
||||||
eventType.recurringEvent &&
|
|
||||||
recurringDatesInfo?.currentRecurringIndex === 0 &&
|
|
||||||
recurringDatesInfo.allRecurringDates
|
|
||||||
) {
|
|
||||||
const allBookingDates = recurringDatesInfo.allRecurringDates.map((strDate) => new Date(strDate));
|
|
||||||
// Go through each date for the recurring event and check if each one's availability
|
|
||||||
// DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
|
|
||||||
// running at the first unavailable time.
|
|
||||||
let i = 0;
|
|
||||||
while (!foundConflict && i < allBookingDates.length) {
|
|
||||||
foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], duration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
log.debug({
|
log.debug({
|
||||||
message: "Unable set isAvailableToBeBooked. Using true. ",
|
message: "Unable set isAvailableToBeBooked. Using true. ",
|
||||||
@@ -882,7 +867,12 @@ async function handler(
|
|||||||
) {
|
) {
|
||||||
const startAsDate = dayjs(reqBody.start).toDate();
|
const startAsDate = dayjs(reqBody.start).toDate();
|
||||||
if (eventType.bookingLimits) {
|
if (eventType.bookingLimits) {
|
||||||
await checkBookingLimits(eventType.bookingLimits as IntervalLimit, startAsDate, eventType.id);
|
await checkBookingLimits(
|
||||||
|
eventType.bookingLimits as IntervalLimit,
|
||||||
|
startAsDate,
|
||||||
|
eventType.id,
|
||||||
|
eventType.schedule?.timeZone
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (eventType.durationLimits) {
|
if (eventType.durationLimits) {
|
||||||
await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id);
|
await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id);
|
||||||
@@ -930,8 +920,8 @@ async function handler(
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dateFrom: reqBody.start,
|
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
|
||||||
dateTo: reqBody.end,
|
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
|
||||||
timeZone: reqBody.timeZone,
|
timeZone: reqBody.timeZone,
|
||||||
originalRescheduledBooking,
|
originalRescheduledBooking,
|
||||||
},
|
},
|
||||||
@@ -998,7 +988,6 @@ async function handler(
|
|||||||
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
|
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
|
||||||
|
|
||||||
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
||||||
|
|
||||||
// use host default
|
// use host default
|
||||||
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
||||||
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
||||||
|
|||||||
@@ -53,24 +53,26 @@ import {
|
|||||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||||
|
import { testWithAndWithoutOrg } from "./lib/test";
|
||||||
|
|
||||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
|
||||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
// Local test runs sometime gets too slow
|
// Local test runs sometime gets too slow
|
||||||
const timeout = process.env.CI ? 5000 : 20000;
|
const timeout = process.env.CI ? 5000 : 20000;
|
||||||
|
|
||||||
describe("handleNewBooking", () => {
|
describe("handleNewBooking", () => {
|
||||||
setupAndTeardown();
|
setupAndTeardown();
|
||||||
|
|
||||||
describe("Fresh/New Booking:", () => {
|
describe("Fresh/New Booking:", () => {
|
||||||
test(
|
testWithAndWithoutOrg(
|
||||||
`should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
|
`should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
|
||||||
1. Should create a booking in the database
|
1. Should create a booking in the database
|
||||||
2. Should send emails to the booker as well as organizer
|
2. Should send emails to the booker as well as organizer
|
||||||
3. Should create a booking in the event's destination calendar
|
3. Should create a booking in the event's destination calendar
|
||||||
3. Should trigger BOOKING_CREATED webhook
|
3. Should trigger BOOKING_CREATED webhook
|
||||||
`,
|
`,
|
||||||
async ({ emails }) => {
|
async ({ emails, org }) => {
|
||||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||||
const booker = getBooker({
|
const booker = getBooker({
|
||||||
email: "booker@example.com",
|
email: "booker@example.com",
|
||||||
@@ -89,37 +91,41 @@ describe("handleNewBooking", () => {
|
|||||||
externalId: "organizer@google-calendar.com",
|
externalId: "organizer@google-calendar.com",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await createBookingScenario(
|
await createBookingScenario(
|
||||||
getScenarioData({
|
getScenarioData(
|
||||||
webhooks: [
|
{
|
||||||
{
|
webhooks: [
|
||||||
userId: organizer.id,
|
{
|
||||||
eventTriggers: ["BOOKING_CREATED"],
|
userId: organizer.id,
|
||||||
subscriberUrl: "http://my-webhook.example.com",
|
eventTriggers: ["BOOKING_CREATED"],
|
||||||
active: true,
|
subscriberUrl: "http://my-webhook.example.com",
|
||||||
eventTypeId: 1,
|
active: true,
|
||||||
appId: null,
|
eventTypeId: 1,
|
||||||
},
|
appId: null,
|
||||||
],
|
|
||||||
eventTypes: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
slotInterval: 45,
|
|
||||||
length: 45,
|
|
||||||
users: [
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
destinationCalendar: {
|
|
||||||
integration: "google_calendar",
|
|
||||||
externalId: "event-type-1@google-calendar.com",
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
eventTypes: [
|
||||||
organizer,
|
{
|
||||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
id: 1,
|
||||||
})
|
slotInterval: 45,
|
||||||
|
length: 45,
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
destinationCalendar: {
|
||||||
|
integration: "google_calendar",
|
||||||
|
externalId: "event-type-1@google-calendar.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizer,
|
||||||
|
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||||
|
},
|
||||||
|
org?.organization
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
mockSuccessfulVideoMeetingCreation({
|
mockSuccessfulVideoMeetingCreation({
|
||||||
@@ -195,6 +201,10 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
urlOrigin: org ? org.urlOrigin : WEBAPP_URL,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
@@ -343,6 +353,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
@@ -488,6 +501,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
@@ -749,6 +765,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
@@ -834,11 +853,14 @@ describe("handleNewBooking", () => {
|
|||||||
const createdBooking = await handleNewBooking(req);
|
const createdBooking = await handleNewBooking(req);
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
// Because no calendar was involved, we don't have an ics UID
|
// Because no calendar was involved, we don't have an ics UID
|
||||||
iCalUID: createdBooking.uid,
|
iCalUID: createdBooking.uid!,
|
||||||
});
|
});
|
||||||
|
|
||||||
expectBookingCreatedWebhookToHaveBeenFired({
|
expectBookingCreatedWebhookToHaveBeenFired({
|
||||||
@@ -1436,6 +1458,9 @@ describe("handleNewBooking", () => {
|
|||||||
expectWorkflowToBeTriggered();
|
expectWorkflowToBeTriggered();
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
@@ -1730,6 +1755,9 @@ describe("handleNewBooking", () => {
|
|||||||
expectWorkflowToBeTriggered();
|
expectWorkflowToBeTriggered();
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
emails,
|
emails,
|
||||||
|
|||||||
+4
-1
@@ -1,11 +1,12 @@
|
|||||||
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||||
|
|
||||||
|
export const DEFAULT_TIMEZONE_BOOKER = "Asia/Kolkata";
|
||||||
export function getBasicMockRequestDataForBooking() {
|
export function getBasicMockRequestDataForBooking() {
|
||||||
return {
|
return {
|
||||||
start: `${getDate({ dateIncrement: 1 }).dateString}T04:00:00.000Z`,
|
start: `${getDate({ dateIncrement: 1 }).dateString}T04:00:00.000Z`,
|
||||||
end: `${getDate({ dateIncrement: 1 }).dateString}T04:30:00.000Z`,
|
end: `${getDate({ dateIncrement: 1 }).dateString}T04:30:00.000Z`,
|
||||||
eventTypeSlug: "no-confirmation",
|
eventTypeSlug: "no-confirmation",
|
||||||
timeZone: "Asia/Calcutta",
|
timeZone: DEFAULT_TIMEZONE_BOOKER,
|
||||||
language: "en",
|
language: "en",
|
||||||
user: "teampro",
|
user: "teampro",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
@@ -20,6 +21,8 @@ export function getMockRequestDataForBooking({
|
|||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
rescheduleUid?: string;
|
rescheduleUid?: string;
|
||||||
bookingUid?: string;
|
bookingUid?: string;
|
||||||
|
recurringEventId?: string;
|
||||||
|
recurringCount?: number;
|
||||||
responses: {
|
responses: {
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { TestFunction } from "vitest";
|
||||||
|
|
||||||
|
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||||
|
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
||||||
|
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||||
|
|
||||||
|
const _testWithAndWithoutOrg = (
|
||||||
|
description: Parameters<typeof testWithAndWithoutOrg>[0],
|
||||||
|
fn: Parameters<typeof testWithAndWithoutOrg>[1],
|
||||||
|
timeout: Parameters<typeof testWithAndWithoutOrg>[2],
|
||||||
|
mode: "only" | "skip" | "run" = "run"
|
||||||
|
) => {
|
||||||
|
const t = mode === "only" ? test.only : mode === "skip" ? test.skip : test;
|
||||||
|
t(
|
||||||
|
`${description} - With org`,
|
||||||
|
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||||
|
const org = await createOrganization({
|
||||||
|
name: "Test Org",
|
||||||
|
slug: "testorg",
|
||||||
|
});
|
||||||
|
|
||||||
|
await fn({
|
||||||
|
meta,
|
||||||
|
task,
|
||||||
|
onTestFailed,
|
||||||
|
expect,
|
||||||
|
emails,
|
||||||
|
skip,
|
||||||
|
org: {
|
||||||
|
organization: org,
|
||||||
|
urlOrigin: `http://${org.slug}.cal.local:3000`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
|
||||||
|
t(
|
||||||
|
`${description}`,
|
||||||
|
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||||
|
await fn({
|
||||||
|
emails,
|
||||||
|
meta,
|
||||||
|
task,
|
||||||
|
onTestFailed,
|
||||||
|
expect,
|
||||||
|
skip,
|
||||||
|
org: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testWithAndWithoutOrg = (
|
||||||
|
description: string,
|
||||||
|
fn: TestFunction<
|
||||||
|
Fixtures & {
|
||||||
|
org: {
|
||||||
|
organization: { id: number | null };
|
||||||
|
urlOrigin?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
timeout?: number
|
||||||
|
) => {
|
||||||
|
_testWithAndWithoutOrg(description, fn, timeout, "run");
|
||||||
|
};
|
||||||
|
|
||||||
|
testWithAndWithoutOrg.only = ((description, fn) => {
|
||||||
|
_testWithAndWithoutOrg(description, fn, "only");
|
||||||
|
}) as typeof _testWithAndWithoutOrg;
|
||||||
|
|
||||||
|
testWithAndWithoutOrg.skip = ((description, fn) => {
|
||||||
|
_testWithAndWithoutOrg(description, fn, "skip");
|
||||||
|
}) as typeof _testWithAndWithoutOrg;
|
||||||
+12
@@ -213,6 +213,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
otherTeamMembers,
|
otherTeamMembers,
|
||||||
@@ -525,6 +528,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
otherTeamMembers,
|
otherTeamMembers,
|
||||||
@@ -842,6 +848,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
otherTeamMembers,
|
otherTeamMembers,
|
||||||
@@ -1056,6 +1065,9 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulBookingCreationEmails({
|
expectSuccessfulBookingCreationEmails({
|
||||||
|
booking: {
|
||||||
|
uid: createdBooking.uid!,
|
||||||
|
},
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
otherTeamMembers,
|
otherTeamMembers,
|
||||||
|
|||||||
@@ -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}>
|
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight} data-testid="view_next_month">
|
||||||
{t("view_next_month")}
|
{t("view_next_month")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
destinationCalendar?: DestinationCalendar | null;
|
destinationCalendar?: DestinationCalendar | null;
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
|
hideAdvancedText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
@@ -51,6 +52,7 @@ const DestinationCalendarSelector = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
value,
|
value,
|
||||||
hidePlaceholder,
|
hidePlaceholder,
|
||||||
|
hideAdvancedText,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
destinationCalendar,
|
destinationCalendar,
|
||||||
}: Props): JSX.Element | null => {
|
}: Props): JSX.Element | null => {
|
||||||
@@ -177,7 +179,9 @@ const DestinationCalendarSelector = ({
|
|||||||
components={{ SingleValue: SingleValueComponent, Option: OptionComponent }}
|
components={{ SingleValue: SingleValueComponent, Option: OptionComponent }}
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm leading-tight">{t("you_can_override_calendar_in_advanced_tab")}</p>
|
{hideAdvancedText ? null : (
|
||||||
|
<p className="text-sm leading-tight">{t("you_can_override_calendar_in_advanced_tab")}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -63,5 +64,22 @@ 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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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:
|
||||||
@@ -21,7 +22,9 @@ export function getAvailableDatesInMonth({
|
|||||||
);
|
);
|
||||||
for (
|
for (
|
||||||
let date = browsingDate > minDate ? browsingDate : minDate;
|
let date = browsingDate > minDate ? browsingDate : minDate;
|
||||||
date <= lastDateOfMonth;
|
// Check if date is before the last date of the month
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { getSession } from "@calcom/features/auth/lib/getSession";
|
import { getSession } from "@calcom/features/auth/lib/getSession";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
const teamIdschema = z.object({
|
const teamIdschema = z.object({
|
||||||
teamId: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive()),
|
teamId: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive()),
|
||||||
@@ -63,11 +64,75 @@ export function checkUserIdentifier(creds: Partial<Credentials>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkPermission(session: Session | null) {
|
export function checkPermission(session: Session | null) {
|
||||||
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
|
if (
|
||||||
|
(session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") ||
|
||||||
|
!session?.user
|
||||||
|
) {
|
||||||
throw new Error("You do not have permission to do this.");
|
throw new Error("You do not have permission to do this.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getImpersonatedUser({
|
||||||
|
session,
|
||||||
|
teamId,
|
||||||
|
creds,
|
||||||
|
}: {
|
||||||
|
session: Session | null;
|
||||||
|
teamId: number | undefined;
|
||||||
|
creds: Credentials | null;
|
||||||
|
}) {
|
||||||
|
let TeamWhereClause: Prisma.MembershipWhereInput = {
|
||||||
|
disableImpersonation: false, // Ensure they have impersonation enabled
|
||||||
|
accepted: true, // Ensure they are apart of the team and not just invited.
|
||||||
|
team: {
|
||||||
|
id: teamId, // Bring back only the right team
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If you are an admin we dont need to follow this flow -> We can just follow the usual flow
|
||||||
|
// If orgId and teamId are the same we can follow the same flow
|
||||||
|
if (session?.user.org?.id && session.user.org.id !== teamId && session?.user.role !== "ADMIN") {
|
||||||
|
TeamWhereClause = {
|
||||||
|
disableImpersonation: false,
|
||||||
|
accepted: true,
|
||||||
|
team: {
|
||||||
|
id: session.user.org.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user who is being impersonated
|
||||||
|
const impersonatedUser = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [{ username: creds?.username }, { email: creds?.username }],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
organizationId: true,
|
||||||
|
disableImpersonation: true,
|
||||||
|
locale: true,
|
||||||
|
teams: {
|
||||||
|
where: TeamWhereClause,
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
disableImpersonation: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!impersonatedUser) {
|
||||||
|
throw new Error("This user does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return impersonatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
const ImpersonationProvider = CredentialsProvider({
|
const ImpersonationProvider = CredentialsProvider({
|
||||||
id: "impersonation-auth",
|
id: "impersonation-auth",
|
||||||
name: "Impersonation",
|
name: "Impersonation",
|
||||||
@@ -85,41 +150,7 @@ const ImpersonationProvider = CredentialsProvider({
|
|||||||
checkUserIdentifier(creds);
|
checkUserIdentifier(creds);
|
||||||
checkPermission(session);
|
checkPermission(session);
|
||||||
|
|
||||||
// Get user who is being impersonated
|
const impersonatedUser = await getImpersonatedUser({ session, teamId, creds });
|
||||||
const impersonatedUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
OR: [{ username: creds?.username }, { email: creds?.username }],
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
role: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
organizationId: true,
|
|
||||||
disableImpersonation: true,
|
|
||||||
locale: true,
|
|
||||||
teams: {
|
|
||||||
where: {
|
|
||||||
disableImpersonation: false, // Ensure they have impersonation enabled
|
|
||||||
accepted: true, // Ensure they are apart of the team and not just invited.
|
|
||||||
team: {
|
|
||||||
id: teamId, // Bring back only the right team
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
teamId: true,
|
|
||||||
disableImpersonation: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if impersonating is allowed for this user
|
|
||||||
if (!impersonatedUser) {
|
|
||||||
throw new Error("This user does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session?.user.role === "ADMIN") {
|
if (session?.user.role === "ADMIN") {
|
||||||
if (impersonatedUser.disableImpersonation) {
|
if (impersonatedUser.disableImpersonation) {
|
||||||
|
|||||||
@@ -23,23 +23,22 @@ export const AddNewOrgAdminsForm = () => {
|
|||||||
}>();
|
}>();
|
||||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
||||||
async onSuccess(data) {
|
async onSuccess(data) {
|
||||||
if (data.sendEmailInvitation) {
|
if (Array.isArray(data.usernameOrEmail)) {
|
||||||
if (Array.isArray(data.usernameOrEmail)) {
|
showToast(
|
||||||
showToast(
|
t("email_invite_team_bulk", {
|
||||||
t("email_invite_team_bulk", {
|
userCount: data.usernameOrEmail.length,
|
||||||
userCount: data.usernameOrEmail.length,
|
}),
|
||||||
}),
|
"success"
|
||||||
"success"
|
);
|
||||||
);
|
} else {
|
||||||
} else {
|
showToast(
|
||||||
showToast(
|
t("email_invite_team", {
|
||||||
t("email_invite_team", {
|
email: data.usernameOrEmail,
|
||||||
email: data.usernameOrEmail,
|
}),
|
||||||
}),
|
"success"
|
||||||
"success"
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`/settings/organizations/${orgId}/add-teams`);
|
router.push(`/settings/organizations/${orgId}/add-teams`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -56,7 +55,6 @@ export const AddNewOrgAdminsForm = () => {
|
|||||||
language: i18n.language,
|
language: i18n.language,
|
||||||
role: MembershipRole.ADMIN,
|
role: MembershipRole.ADMIN,
|
||||||
usernameOrEmail: values.emails,
|
usernameOrEmail: values.emails,
|
||||||
sendEmailInvitation: true,
|
|
||||||
isOrg: true,
|
isOrg: true,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import type { IncomingMessage } from "http";
|
||||||
|
|
||||||
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
|
||||||
|
const log = logger.getSubLogger({
|
||||||
|
prefix: ["orgDomains.ts"],
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* return the org slug
|
* return the org slug
|
||||||
* @param hostname
|
* @param hostname
|
||||||
*/
|
*/
|
||||||
export function getOrgSlug(hostname: string) {
|
export function getOrgSlug(hostname: string, forcedSlug?: string) {
|
||||||
|
if (forcedSlug) {
|
||||||
|
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||||
|
log.debug("Using provided forcedSlug in E2E", {
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
return forcedSlug;
|
||||||
|
}
|
||||||
|
log.debug("Ignoring forcedSlug in non-test mode", {
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!hostname.includes(".")) {
|
if (!hostname.includes(".")) {
|
||||||
// A no-dot domain can never be org domain. It automatically handles localhost
|
log.warn('Org support not enabled for hostname without "."', { hostname });
|
||||||
|
// A no-dot domain can never be org domain. It automatically considers localhost to be non-org domain
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Find which hostname is being currently used
|
// Find which hostname is being currently used
|
||||||
@@ -19,24 +36,45 @@ export function getOrgSlug(hostname: string) {
|
|||||||
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
||||||
return testHostname.endsWith(`.${ahn}`);
|
return testHostname.endsWith(`.${ahn}`);
|
||||||
});
|
});
|
||||||
logger.debug(`getOrgSlug: ${hostname} ${currentHostname}`, {
|
|
||||||
ALLOWED_HOSTNAMES,
|
if (!currentHostname) {
|
||||||
WEBAPP_URL,
|
log.warn("Match of WEBAPP_URL with ALLOWED_HOSTNAME failed", { WEBAPP_URL, ALLOWED_HOSTNAMES });
|
||||||
currentHostname,
|
return null;
|
||||||
hostname,
|
|
||||||
});
|
|
||||||
if (currentHostname) {
|
|
||||||
// Define which is the current domain/subdomain
|
|
||||||
const slug = hostname.replace(`.${currentHostname}` ?? "", "");
|
|
||||||
return slug.indexOf(".") === -1 ? slug : null;
|
|
||||||
}
|
}
|
||||||
|
// Define which is the current domain/subdomain
|
||||||
|
const slug = hostname.replace(`.${currentHostname}` ?? "", "");
|
||||||
|
const hasNoDotInSlug = slug.indexOf(".") === -1;
|
||||||
|
if (hasNoDotInSlug) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
log.warn("Derived slug ended up having dots, so not considering it an org domain", { slug });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orgDomainConfig(hostname: string, fallback?: string | string[]) {
|
export function orgDomainConfig(req: IncomingMessage | undefined, fallback?: string | string[]) {
|
||||||
const currentOrgDomain = getOrgSlug(hostname);
|
const forcedSlugHeader = req?.headers?.["x-cal-force-slug"];
|
||||||
|
|
||||||
|
const forcedSlug = forcedSlugHeader instanceof Array ? forcedSlugHeader[0] : forcedSlugHeader;
|
||||||
|
|
||||||
|
const hostname = req?.headers?.host || "";
|
||||||
|
return getOrgDomainConfigFromHostname({
|
||||||
|
hostname,
|
||||||
|
fallback,
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrgDomainConfigFromHostname({
|
||||||
|
hostname,
|
||||||
|
fallback,
|
||||||
|
forcedSlug,
|
||||||
|
}: {
|
||||||
|
hostname: string;
|
||||||
|
fallback?: string | string[];
|
||||||
|
forcedSlug?: string;
|
||||||
|
}) {
|
||||||
|
const currentOrgDomain = getOrgSlug(hostname, forcedSlug);
|
||||||
const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain);
|
const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain);
|
||||||
logger.debug(`orgDomainConfig: ${hostname} ${currentOrgDomain} ${isValidOrgDomain}`);
|
|
||||||
if (isValidOrgDomain || !fallback) {
|
if (isValidOrgDomain || !fallback) {
|
||||||
return {
|
return {
|
||||||
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
|
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
|
||||||
@@ -58,7 +96,10 @@ export function subdomainSuffix() {
|
|||||||
|
|
||||||
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
||||||
if (!slug) return WEBAPP_URL;
|
if (!slug) return WEBAPP_URL;
|
||||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
|
const orgFullOrigin = `${
|
||||||
|
options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""
|
||||||
|
}${slug}.${subdomainSuffix()}`;
|
||||||
|
return orgFullOrigin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,6 +141,6 @@ export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function userOrgQuery(hostname: string, fallback?: string | string[]) {
|
export function userOrgQuery(hostname: string, fallback?: string | string[]) {
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(hostname, fallback);
|
const { currentOrgDomain, isValidOrgDomain } = getOrgDomainConfigFromHostname({ hostname, fallback });
|
||||||
return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null;
|
return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null;
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ const OrgAppearanceView = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border-subtle rounded-md border p-5">
|
<div className="py-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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -189,29 +189,27 @@ const MembersView = () => {
|
|||||||
language: i18n.language,
|
language: i18n.language,
|
||||||
role: values.role,
|
role: values.role,
|
||||||
usernameOrEmail: values.emailOrUsername,
|
usernameOrEmail: values.emailOrUsername,
|
||||||
sendEmailInvitation: values.sendInviteEmail,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
await utils.viewer.teams.get.invalidate();
|
await utils.viewer.teams.get.invalidate();
|
||||||
setShowMemberInvitationModal(false);
|
setShowMemberInvitationModal(false);
|
||||||
if (data.sendEmailInvitation) {
|
|
||||||
if (Array.isArray(data.usernameOrEmail)) {
|
if (Array.isArray(data.usernameOrEmail)) {
|
||||||
showToast(
|
showToast(
|
||||||
t("email_invite_team_bulk", {
|
t("email_invite_team_bulk", {
|
||||||
userCount: data.usernameOrEmail.length,
|
userCount: data.usernameOrEmail.length,
|
||||||
}),
|
}),
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
resetFields();
|
resetFields();
|
||||||
} else {
|
} else {
|
||||||
showToast(
|
showToast(
|
||||||
t("email_invite_team", {
|
t("email_invite_team", {
|
||||||
email: data.usernameOrEmail,
|
email: data.usernameOrEmail,
|
||||||
}),
|
}),
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user