Compare commits

...
35 Commits
Author SHA1 Message Date
Keith Williams 5c8abbb3e9 v3.4.7
release.yml / release (push) Canceled after 0s
2023-11-07 15:07:42 -03:00
Carina WollendorferandCarinaWolli 7bb42ba577 fix: booking limits (#12172)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-07 14:17:05 -03:00
Keith WilliamsandPeer Richelsen 2e3b3257db feat: Add Sentry (#12155)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-07 13:18:25 -03:00
gitstart-app[bot]andgitstart-calcom d28e20e92e fix: Fix "Could not book the meeting" issue (fix-bookingIssue) (#12252)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
2023-11-07 12:33:49 -03:00
Carina WollendorferandCarinaWolli 8b89d6635a fix: recurring events with calendar connections (#12250)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-07 12:09:56 -03:00
Benny Joo 13ec810cb9 fix: [app dir bootstrapping 8] useParamsWithFallback hook and add tests (#12041)
* fix: first solution using RouterContext

* fix: second solution by importing router from next/compat/router

* fix return type
2023-11-07 13:48:02 +00:00
Morgan 367c8a96d8 feat: Add user's avatar to POST and PATCH requests in users endpoints (#11187) (#12256)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Fix wrong import introduced in this pr: #11187

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Requirement/Documentation

<!-- Please provide all documents that are important to understand the reason of that PR. -->

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

<!-- Please delete bullets that are not relevant. -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->

- Are there environment variables that should be set?
- What are the minimal test data to have?
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR

## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

<!-- 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)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes
2023-11-07 10:21:20 +00:00
e94c030c45 feat: Add user's avatar to POST and PATCH requests in users endpoints (#11187)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-07 08:06:52 +00:00
MorganandKeith Williams 6d0c7e4214 chore: init infra folder with dockerized api (#12243)
* chore(infra): scripts and dockerfile for api

* chore(infra): basic readme file

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-11-07 09:47:06 +02:00
Carina WollendorferandCarinaWolli e11cb9e3a5 fix: unhandled promise error in scheduleEmailReminders endpoint (#12183)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-06 09:14:35 -05:00
Manpreet Singh 809abd2f4f fix: Removes send an invite email option & always send email (#12229) 2023-11-06 13:30:37 +05:30
d1d50b0d91 test: Create E2E tests for bookings with custom/required Multiple Emails + other questions (teste2e-multipleEmailQuestion) (#11565)
* Remove unnecessary changes

* add changes

* Requested changes

* Requested changes

* fix: rename file to have .e2e

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2023-11-03 15:29:47 +00:00
Lauris Skraucis fc716f5921 fix: booking timeslots (#12195) 2023-11-03 10:58:58 -04:00
5348464972 test: Create E2E tests for bookings for all questions together as custom/required (teste2e-allQuestions) (#11592)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2023-11-03 11:12:17 -03:00
3fac6e698e test: Create E2E tests for bookings with custom/required Radio Group + other questions (teste2e-radioGroupQuestion) (#11567)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2023-11-03 11:11:18 -03:00
dcafbf672a test: Create E2E tests for bookings with custom/required Checkbox Group + other questions (teste2e-checkboxGroupQuestion) (#11568)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2023-11-03 11:10:17 -03:00
Rahul RoyandRahul Roy dff89a4df8 feat: #11922 Removed error for cancelling past time slots (#12126)
Co-authored-by: Rahul Roy <rahulro@maqsoftware.com>
2023-11-03 11:29:41 +00:00
Syed Ali Shahbaz ed04062540 fix: API to return hosts on event types response (#12204) 2023-11-03 11:28:16 +00:00
Siddharth MovaliyaandPeer Richelsen 23fda4a9e6 feat: The 'confirm' button triggers after entering the code (#12128)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-03 11:27:52 +00:00
Udit Takkar 14c3028c2a fix: use 18n 2023-11-03 11:27:09 +00:00
Udit Takkar dfe49e1732 fix: link input 2023-11-03 11:27:09 +00:00
Udit Takkar 6148c6baea chore: add placeholder 2023-11-03 11:27:09 +00:00
Udit Takkar 5366d891bb fix: improve error message 2023-11-03 11:27:09 +00:00
Sean Brydon 2ee021a87f fix:installed-calendar-info 2023-11-03 10:23:50 +00:00
Alex van Andel dd0c2dbff2 fix: Set the dateFrom to startOf day otherwise bad fromOffset is used (#12210) 2023-11-03 03:13:42 +00:00
Hariom Balhara f044c2d7c7 fix: Embed: data-cal-origin not used by modal(which is used by both element click popup and floating button popup) (#12075) 2023-11-02 19:39:58 +00:00
Somay Chauhanandsean-brydon 703bcd861e fix: should allow org owners and admins to delete members from subteams (#11710)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-11-02 14:37:19 -04:00
Syed Ali Shahbaz d4d96eb5cd fix admin check (#12175) 2023-11-02 12:29:15 +00:00
Hariom Balhara d8356e9a5b fix: Make sure if header is set already, it isn't set again (#12194) 2023-11-02 09:22:16 -03:00
Alexander Zeitler 9348279818 docs(README): remove unpaired symbol (#12133) 2023-11-02 09:36:47 +00:00
Peer Richelsen 5d77e4ce1d chore: changed readme (#12171) 2023-11-02 09:36:00 +00:00
Syed Ali Shahbaz 968e782dbe Update README.md (#12176) 2023-11-02 09:34:59 +00:00
Udit Takkar 01fb3dd03b fix: hide Check for recording (#12184) 2023-11-02 09:34:25 +00:00
Lauris Skraucis c3537f3290 docs: needs approval label (#12177)
Our CI workflow doesn't allow merging for only .md changes because the required check doesn't clear based on file filters.
2023-11-01 11:52:11 -03:00
3bb42f276e fix: Attach credentialId to location option (#12086)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-11-01 09:34:05 -04:00
74 changed files with 2724 additions and 510 deletions
+5 -1
View File
@@ -115,6 +115,10 @@ SENDGRID_API_KEY=
SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Sentry
# Used for capturing exceptions and logging messages
NEXT_PUBLIC_SENTRY_DSN=
# Twilio
# Used to send SMS reminders in workflows
TWILIO_SID=
@@ -126,7 +130,7 @@ TWILIO_WHATSAPP_PHONE_NUMBER=
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
# Set it to "1" if you need to run E2E tests locally.
# Set it to "1" if you need to run E2E tests locally.
NEXT_PUBLIC_IS_E2E=
# Used for internal billing system
@@ -47,3 +47,9 @@ assignees: ""
-->
(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.
+1 -1
View File
@@ -40,7 +40,7 @@ Fixes # (issue)
## 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)
- My code doesn't follow the style guidelines of this project
+12 -1
View File
@@ -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**.
- 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
+2 -4
View File
@@ -122,7 +122,7 @@ Here is what you need to be able to run Cal.com.
### 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
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. 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`)
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:
[<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)
- [Next.js](https://nextjs.org/)
- [Day.js](https://day.js.org/)
+4
View File
@@ -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
View File
@@ -4,6 +4,7 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
import { timeZone } from "~/lib/validations/shared/timeZone";
// @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()
.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,
@@ -128,6 +130,7 @@ const schemaUserCreateParams = z.object({
.nullable(),
locale: z.nativeEnum(locales).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,
+2 -1
View File
@@ -13,6 +13,7 @@
"lint": "eslint . --ignore-path .gitignore",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "PORT=3002 next start",
"docker-start-api": "PORT=80 next start",
"type-check": "tsc --pretty --noEmit"
},
"devDependencies": {
@@ -29,7 +30,7 @@
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/trpc": "*",
"@sentry/nextjs": "^7.20.0",
"@sentry/nextjs": "^7.73.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"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>
* </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>The userId did not matched an existing user.</td>
* </tr>
@@ -51,6 +51,7 @@ export async function getHandler(req: NextApiRequest) {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
+1
View File
@@ -45,6 +45,7 @@ async function getHandler(req: NextApiRequest) {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
+3 -2
View File
@@ -316,8 +316,9 @@ async function checkPermissions(req: NextApiRequest) {
statusCode: 401,
message: "ADMIN required for `userId`",
});
/* Admin users are required to pass in a userId */
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
/* Admin users are required to pass in a userId or teamId */
if (isAdmin && (!body.userId || !body.teamId))
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
}
export default defaultResponder(postHandler);
@@ -65,6 +65,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
* 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]
* type: string
* avatar:
* description: The user's avatar, in base64 format
* type: string
* examples:
* user:
* summary: An example of USER
+3
View File
@@ -60,6 +60,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
* 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]
* type: string
* avatar:
* description: The user's avatar, in base64 format
* type: string
* examples:
* user:
* summary: An example of USER
+1 -2
View File
@@ -631,8 +631,7 @@ paths:
type: string
role:
type: string
sendEmailInvitation:
type: boolean
parameters:
- schema:
type: string
@@ -221,6 +221,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
hidePlaceholder
isLoading={mutation.isLoading}
value={data.destinationCalendar?.externalId}
hideAdvancedText
/>
</div>
</div>
@@ -261,13 +261,11 @@ function BookingListItem(booking: BookingItemProps) {
const title = booking.title;
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed);
const checkForRecordingsButton =
!showRecordingsButtons && (booking.location === "integrations:daily" || booking?.location?.trim() === "");
const showRecordingActions: ActionType[] = [
{
id: checkForRecordingsButton ? "check_for_recordings" : "view_recordings",
label: checkForRecordingsButton ? t("check_for_recordings") : t("view_recordings"),
id: "view_recordings",
label: t("view_recordings"),
onClick: () => {
setViewRecordingsDialogIsOpen(true);
},
@@ -298,7 +296,7 @@ function BookingListItem(booking: BookingItemProps) {
paymentCurrency={booking.payment[0].currency}
/>
)}
{(showRecordingsButtons || checkForRecordingsButton) && (
{showRecordingsButtons && (
<ViewRecordingsDialog
booking={booking}
isOpenDialog={viewRecordingsDialogIsOpen}
@@ -468,9 +466,7 @@ function BookingListItem(booking: BookingItemProps) {
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{(showRecordingsButtons || checkForRecordingsButton) && (
<TableActions actions={showRecordingActions} />
)}
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />
@@ -356,9 +356,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
if (val.credential) {
locationFormMethods.setValue("credentialId", val.credential.id);
locationFormMethods.setValue("teamName", val.credential.team?.name);
if (!!val.credentialId) {
locationFormMethods.setValue("credentialId", val.credentialId);
locationFormMethods.setValue("teamName", val.teamName);
}
locationFormMethods.unregister([
+51 -42
View File
@@ -30,8 +30,7 @@ import {
Button,
showToast,
} from "@calcom/ui";
import { Plus, X, Check } from "@calcom/ui/components/icon";
import { CornerDownRight } from "@calcom/ui/components/icon";
import { Plus, X, Check, CornerDownRight } from "@calcom/ui/components/icon";
import CheckboxField from "@components/ui/form/CheckboxField";
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
@@ -200,23 +199,16 @@ export const EventSetupTab = (
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<>
<Input
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
type="text"
required
onChange={onChange}
value={value}
className="my-0"
{...rest}
/>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 text-sm"
as="div"
/>
</>
<Input
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
type="text"
required
onChange={onChange}
value={value}
className="my-0"
{...rest}
/>
);
}}
/>
@@ -231,21 +223,14 @@ export const EventSetupTab = (
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
<>
<PhoneInput
required
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
value={value}
onChange={onChange}
{...rest}
/>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 text-sm"
as="div"
/>
</>
<PhoneInput
required
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
value={value}
onChange={onChange}
{...rest}
/>
);
}}
/>
@@ -298,9 +283,21 @@ export const EventSetupTab = (
!validLocations.find((location) => location.type === newLocationType);
if (canAddLocation) {
updateLocationField(index, { type: newLocationType });
updateLocationField(index, {
type: newLocationType,
...(e.credentialId && {
credentialId: e.credentialId,
teamName: e.teamName,
}),
});
} 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");
}
}
@@ -320,11 +317,11 @@ export const EventSetupTab = (
{eventLocationType?.organizerInputType && (
<div className="mt-2 space-y-2">
<div className="flex gap-2">
<div className="flex items-center justify-center">
<CornerDownRight className="h-4 w-4" />
</div>
<div className="w-full">
<div className="w-full">
<div className="flex gap-2">
<div className="flex items-center justify-center">
<CornerDownRight className="h-4 w-4" />
</div>
<LocationInput
defaultValue={
defaultLocation
@@ -335,6 +332,12 @@ export const EventSetupTab = (
index={index}
/>
</div>
<ErrorMessage
errors={formMethods.formState.errors.locations?.[index]}
name={eventLocationType.defaultValueVariable}
className="text-error my-1 ml-6 text-sm"
as="div"
/>
</div>
<div className="ml-6">
<CheckboxField
@@ -382,7 +385,13 @@ export const EventSetupTab = (
!validLocations.find((location) => location.type === newLocationType);
if (canAppendLocation) {
append({ type: newLocationType });
append({
type: newLocationType,
...(e.credentialId && {
credentialId: e.credentialId,
teamName: e.teamName,
}),
});
setSelectedNewOption(e);
} else {
showToast(t("location_already_exists"), "warning");
@@ -2,7 +2,6 @@ import type { GroupBase, Props, SingleValue } from "react-select";
import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
import { classNames } from "@calcom/lib";
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
import { Select } from "@calcom/ui";
@@ -13,7 +12,8 @@ export type LocationOption = {
icon?: string;
disabled?: boolean;
address?: string;
credential?: CredentialDataWithTeamName;
credentialId?: number;
teamName?: string;
};
export type SingleValueLocationOption = SingleValue<LocationOption>;
+11
View File
@@ -3,6 +3,7 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
const os = require("os");
const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { withSentryConfig } = require("@sentry/nextjs");
const { version } = require("./package.json");
const { i18n } = require("./next-i18next.config");
const {
@@ -92,6 +93,7 @@ if (process.env.ANALYZE === "true") {
}
plugins.push(withAxiom);
const matcherConfigRootPath = {
has: [
{
@@ -547,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);
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.4.6",
"version": "3.4.7",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -57,6 +57,7 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.73.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
@@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { getEventLocationType } from "@calcom/app-store/locations";
import { validateCustomEventName } from "@calcom/core/event";
import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
@@ -321,6 +322,47 @@ const EventTypePage = (props: EventTypeSetupProps) => {
teamName: z.string().optional(),
})
.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(),
})
@@ -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", () => {
const bookingOptions = { hasPlaceholder: true, isRequired: true };
test("Phone and Short text required", async ({ bookingPage }) => {
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
@@ -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);
});
});
});
});
@@ -15,6 +15,7 @@ type BookingOptions = {
hasPlaceholder?: boolean;
isReschedule?: boolean;
isRequired?: boolean;
isAllRequired?: boolean;
isMultiSelect?: boolean;
};
@@ -116,6 +117,61 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
}
};
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) {
const pro = await users.create({ name: "testuser" });
await pro.apiLogin();
@@ -182,6 +238,7 @@ export function createBookingPageFixture(page: Page) {
selectFirstAvailableTime: async () => {
await page.getByTestId("time").first().click();
},
fillRescheduleReasonAndConfirm: async () => {
await page.getByPlaceholder(reschedulePlaceholderText).click();
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
@@ -207,6 +264,14 @@ export function createBookingPageFixture(page: Page) {
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click();
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
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) => {
@@ -261,6 +326,32 @@ export function createBookingPageFixture(page: Page) {
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
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);
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
await expect(scheduleSuccessfullyPage).toBeVisible();
@@ -1222,6 +1222,7 @@
"organizer_name_variable": "Organizer name",
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
"invalid_number": "Invalid phone number",
"invalid_url_error_message": "Invalid URL for {{label}}. Sample URL: {{sampleUrl}}",
"navigate": "Navigate",
"open": "Open",
"close": "Close",
+1
View File
@@ -0,0 +1 @@
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+6
View File
@@ -0,0 +1,6 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0,
});
+119
View File
@@ -4,6 +4,7 @@ import prismock from "../../../../tests/libs/__mocks__/prisma";
import { diff } from "jest-diff";
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
import dayjs from "@calcom/dayjs";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
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", () => {
@@ -101,6 +101,9 @@ export type InputEventType = {
requiresConfirmation?: boolean;
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
schedule?: InputUser["schedules"][number];
bookingLimits?: {
PER_DAY?: number;
};
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
type WhiteListedBookingProps = {
@@ -199,6 +202,7 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser
timeZone: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
bookingLimits: {},
schedulingType: null,
length: 15,
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
+3
View File
@@ -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.
+60
View File
@@ -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"]
+4 -1
View File
@@ -71,7 +71,10 @@
"test": "vitest run",
"type-check": "turbo run type-check",
"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": {
"@changesets/cli": "^2.26.1",
+7 -1
View File
@@ -88,7 +88,13 @@ export async function getLocationGroupedOptions(
teamName: credential.team?.name,
}))) {
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]) {
apps[groupByCategory] = [...apps[groupByCategory], option];
} else {
@@ -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;
}
@@ -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()
);
}
@@ -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);
};
+5 -1
View File
@@ -236,7 +236,7 @@ export class Cal {
}: {
calLink: string;
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
calOrigin?: string;
calOrigin: string | null;
}) {
const iframe = (this.iframe = document.createElement("iframe"));
iframe.className = "cal-embed";
@@ -473,10 +473,12 @@ class CalApi {
}
config.embedType = "inline";
const calConfig = this.cal.getConfig();
const iframe = this.cal.createIframe({
calLink,
queryObject: withColorScheme(Cal.getQueryObject(config), containerEl),
calOrigin: calConfig.calOrigin,
});
iframe.style.height = "100%";
@@ -555,6 +557,7 @@ class CalApi {
modal({
calLink,
config = {},
calOrigin,
__prerender = false,
}: {
calLink: string;
@@ -607,6 +610,7 @@ class CalApi {
iframe = this.cal.createIframe({
calLink,
queryObject,
calOrigin: calOrigin || null,
});
}
@@ -421,6 +421,7 @@ export const BookEventFormChild = ({
onSuccess={() => {
setVerifiedEmail(email);
setEmailVerificationModalVisible(false);
bookEvent(bookingForm.getValues());
}}
isUserSessionRequiredToVerify={false}
/>
@@ -129,10 +129,6 @@ async function handler(req: CustomRequest) {
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) {
throw new HttpError({ statusCode: 400, message: "User not found" });
}
@@ -408,22 +408,7 @@ async function ensureAvailableUsers(
let foundConflict = false;
try {
if (
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);
}
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
} catch {
log.debug({
message: "Unable set isAvailableToBeBooked. Using true. ",
@@ -882,7 +867,12 @@ async function handler(
) {
const startAsDate = dayjs(reqBody.start).toDate();
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) {
await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id);
@@ -930,8 +920,8 @@ async function handler(
}),
},
{
dateFrom: reqBody.start,
dateTo: reqBody.end,
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
@@ -998,7 +988,6 @@ async function handler(
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
// use host default
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
@@ -17,6 +17,7 @@ interface Props {
destinationCalendar?: DestinationCalendar | null;
value: string | undefined;
maxWidth?: number;
hideAdvancedText?: boolean;
}
interface Option {
@@ -51,6 +52,7 @@ const DestinationCalendarSelector = ({
isLoading,
value,
hidePlaceholder,
hideAdvancedText,
maxWidth,
destinationCalendar,
}: Props): JSX.Element | null => {
@@ -177,7 +179,9 @@ const DestinationCalendarSelector = ({
components={{ SingleValue: SingleValueComponent, Option: OptionComponent }}
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>
);
};
@@ -23,23 +23,22 @@ export const AddNewOrgAdminsForm = () => {
}>();
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
async onSuccess(data) {
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
router.push(`/settings/organizations/${orgId}/add-teams`);
},
onError: (error) => {
@@ -56,7 +55,6 @@ export const AddNewOrgAdminsForm = () => {
language: i18n.language,
role: MembershipRole.ADMIN,
usernameOrEmail: values.emails,
sendEmailInvitation: true,
isOrg: true,
});
}}>
@@ -189,29 +189,27 @@ const MembersView = () => {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
sendEmailInvitation: values.sendInviteEmail,
},
{
onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate();
setShowMemberInvitationModal(false);
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
@@ -119,29 +119,26 @@ export const AddNewTeamMembersForm = ({
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
sendEmailInvitation: values.sendInviteEmail,
},
{
onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate();
setMemberInviteModal(false);
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
@@ -13,7 +13,6 @@ import type { RouterOutputs } from "@calcom/trpc";
import { trpc } from "@calcom/trpc";
import {
Button,
CheckboxField,
Dialog,
DialogContent,
DialogFooter,
@@ -53,7 +52,6 @@ type MembershipRoleOption = {
export interface NewMemberForm {
emailOrUsername: string | string[];
role: MembershipRole;
sendInviteEmail: boolean;
}
type ModalMode = "INDIVIDUAL" | "BULK" | "ORGANIZATION";
@@ -344,19 +342,6 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
</div>
)}
/>
<Controller
name="sendInviteEmail"
control={newMemberFormMethods.control}
defaultValue={true}
render={() => (
<CheckboxField
className="mr-0"
defaultChecked={true}
description={t("send_invite_email")}
onChange={(e) => newMemberFormMethods.setValue("sendInviteEmail", e.target.checked)}
/>
)}
/>
{props.token && (
<div className="flex">
<Button
@@ -37,6 +37,7 @@ import TeamPill, { TeamRole } from "./TeamPill";
interface Props {
team: RouterOutputs["viewer"]["teams"]["get"];
member: RouterOutputs["viewer"]["teams"]["get"]["members"][number];
isOrgAdminOrOwner: boolean | undefined;
}
/** TODO: Migrate the one in apps/web to tRPC package */
@@ -109,7 +110,8 @@ export default function MemberListItem(props: Props) {
(props.member.role !== MembershipRole.OWNER ||
ownersInTeam() > 1 ||
props.member.id !== currentUserId)) ||
(props.team.membership?.role === MembershipRole.ADMIN && props.member.role !== MembershipRole.OWNER);
(props.team.membership?.role === MembershipRole.ADMIN && props.member.role !== MembershipRole.OWNER) ||
props.isOrgAdminOrOwner;
const impersonationMode =
editMode &&
!props.member.disableImpersonation &&
@@ -131,29 +131,27 @@ export default function TeamListItem(props: Props) {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
sendEmailInvitation: values.sendInviteEmail,
},
{
onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate();
setOpenMemberInvitationModal(false);
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
@@ -22,13 +22,14 @@ type Team = RouterOutputs["viewer"]["teams"]["get"];
interface MembersListProps {
team: Team | undefined;
isOrgAdminOrOwner: boolean | undefined;
}
const checkIfExist = (comp: string, query: string) =>
comp.toLowerCase().replace(/\s+/g, "").includes(query.toLowerCase().replace(/\s+/g, ""));
function MembersList(props: MembersListProps) {
const { team } = props;
const { team, isOrgAdminOrOwner } = props;
const { t } = useLocale();
const [query, setQuery] = useState<string>("");
@@ -56,7 +57,14 @@ function MembersList(props: MembersListProps) {
{membersList?.length && team ? (
<ul className="divide-subtle border-subtle divide-y rounded-md border ">
{membersList.map((member) => {
return <MemberListItem key={member.id} team={team} member={member} />;
return (
<MemberListItem
key={member.id}
team={team}
member={member}
isOrgAdminOrOwner={isOrgAdminOrOwner}
/>
);
})}
</ul>
) : null}
@@ -161,7 +169,7 @@ const MembersView = () => {
{((team?.isPrivate && isAdmin) || !team?.isPrivate || isOrgAdminOrOwner) && (
<>
<MembersList team={team} />
<MembersList team={team} isOrgAdminOrOwner={isOrgAdminOrOwner} />
<hr className="border-subtle my-8" />
</>
)}
@@ -197,29 +205,27 @@ const MembersView = () => {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
sendEmailInvitation: values.sendInviteEmail,
},
{
onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate();
setShowMemberInvitationModal(false);
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
@@ -1,23 +1,18 @@
/* Schedule any workflow reminder that falls within 72 hours for email */
import type { Prisma } from "@prisma/client";
import client from "@sendgrid/client";
import sgMail from "@sendgrid/mail";
import { createEvent } from "ics";
import type { DateArray } from "ics";
import type { NextApiRequest, NextApiResponse } from "next";
import { RRule } from "rrule";
import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { parseRecurringEvent } from "@calcom/lib";
import { defaultHandler } from "@calcom/lib/server";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import type { User } from "@calcom/prisma/client";
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { getiCalEventAsString } from "../lib/getiCalEventAsString";
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
import customTemplate from "../lib/reminders/templates/customTemplate";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
@@ -28,69 +23,6 @@ const senderEmail = process.env.SENDGRID_EMAIL as string;
sgMail.setApiKey(sendgridAPIKey);
client.setApiKey(sendgridAPIKey);
type Booking = Prisma.BookingGetPayload<{
include: {
eventType: true;
attendees: true;
};
}>;
function getiCalEventAsString(
booking: Pick<Booking, "startTime" | "endTime" | "description" | "location" | "attendees"> & {
eventType: { recurringEvent?: Prisma.JsonValue; title?: string } | null;
user: Partial<User> | null;
}
) {
let recurrenceRule: string | undefined = undefined;
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
if (recurringEvent?.count) {
recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", "");
}
const uid = uuidv4();
const icsEvent = createEvent({
uid,
startInputType: "utc",
start: dayjs(booking.startTime.toISOString() || "")
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
duration: {
minutes: dayjs(booking.endTime.toISOString() || "").diff(
dayjs(booking.startTime.toISOString() || ""),
"minute"
),
},
title: booking.eventType?.title || "",
description: booking.description || "",
location: booking.location || "",
organizer: {
email: booking.user?.email || "",
name: booking.user?.name || "",
},
attendees: [
{
name: booking.attendees[0].name,
email: booking.attendees[0].email,
partstat: "ACCEPTED",
role: "REQ-PARTICIPANT",
rsvp: true,
},
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
@@ -105,11 +37,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false;
// delete batch_ids with already past scheduled date from scheduled_sends
const pageSize = 90;
let pageNumber = 0;
const deletePromises = [];
const deletePromises: Promise<any>[] = [];
//delete batch_ids with already past scheduled date from scheduled_sends
while (true) {
const remindersToDelete = await prisma.workflowReminder.findMany({
where: {
@@ -130,14 +62,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
break;
}
deletePromises.push(
remindersToDelete.map((reminder) =>
client.request({
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
method: "DELETE",
})
)
);
for (const reminder of remindersToDelete) {
const deletePromise = client.request({
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
method: "DELETE",
});
deletePromises.push(deletePromise);
}
pageNumber++;
}
@@ -149,6 +81,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
});
//delete workflow reminders with past scheduled date
await prisma.workflowReminder.deleteMany({
where: {
method: WorkflowMethods.EMAIL,
@@ -159,10 +92,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
pageNumber = 0;
const allPromisesCancelReminders = [];
const allPromisesCancelReminders: Promise<any>[] = [];
while (true) {
const remindersToCancel = await prisma.workflowReminder.findMany({
@@ -217,11 +149,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
});
// schedule all unscheduled reminders within the next 72 hours
pageNumber = 0;
const sendEmailPromises = [];
const sendEmailPromises: Promise<any>[] = [];
while (true) {
//find all unscheduled Email reminders
const unscheduledReminders = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.EMAIL,
@@ -0,0 +1,71 @@
import { createEvent } from "ics";
import type { DateArray } from "ics";
import { RRule } from "rrule";
import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { parseRecurringEvent } from "@calcom/lib";
import type { Prisma, User } from "@calcom/prisma/client";
type Booking = Prisma.BookingGetPayload<{
include: {
eventType: true;
attendees: true;
};
}>;
export function getiCalEventAsString(
booking: Pick<Booking, "startTime" | "endTime" | "description" | "location" | "attendees"> & {
eventType: { recurringEvent?: Prisma.JsonValue; title?: string } | null;
user: Partial<User> | null;
}
) {
let recurrenceRule: string | undefined = undefined;
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
if (recurringEvent?.count) {
recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", "");
}
const uid = uuidv4();
const icsEvent = createEvent({
uid,
startInputType: "utc",
start: dayjs(booking.startTime.toISOString() || "")
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
duration: {
minutes: dayjs(booking.endTime.toISOString() || "").diff(
dayjs(booking.startTime.toISOString() || ""),
"minute"
),
},
title: booking.eventType?.title || "",
description: booking.description || "",
location: booking.location || "",
organizer: {
email: booking.user?.email || "",
name: booking.user?.name || "",
},
attendees: [
{
name: booking.attendees[0].name,
email: booking.attendees[0].email,
partstat: "ACCEPTED",
role: "REQ-PARTICIPANT",
rsvp: true,
},
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
@@ -23,22 +23,21 @@ export function InviteMemberModal(props: Props) {
// loaded a bunch of data and idk how pagination works with invalidation. We may need to use
// Optimistic updates here instead.
await utils.viewer.organizations.listMembers.invalidate();
if (data.sendEmailInvitation) {
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.usernameOrEmail.length,
}),
"success"
);
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
@@ -69,7 +68,6 @@ export function InviteMemberModal(props: Props) {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
sendEmailInvitation: values.sendInviteEmail,
isOrg: true,
});
}}
+133 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
@@ -48,6 +48,106 @@ describe("processWorkingHours", () => {
expect(lastAvailableSlot.start.date()).toBe(31);
});
it("It has the correct working hours on date of DST change (- tz)", () => {
vi.useFakeTimers().setSystemTime(new Date("2023-11-05T13:26:14.000Z"));
const item = {
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 9, 0)), // 9 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
const timeZone = "America/New_York";
const dateFrom = dayjs();
const dateTo = dayjs().endOf("month");
const results = processWorkingHours({ item, timeZone, dateFrom, dateTo });
expect(results).toStrictEqual([
{
start: dayjs("2023-11-06T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-06T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-07T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-07T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-08T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-08T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-09T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-09T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-10T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-10T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-13T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-13T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-14T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-14T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-15T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-15T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-16T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-16T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-17T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-17T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-20T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-20T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-21T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-21T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-22T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-22T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-23T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-23T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-24T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-24T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-27T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-27T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-28T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-28T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-29T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-29T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-30T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-30T22:00:00.000Z").tz(timeZone),
},
]);
vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers();
});
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
it.skip("should return the correct working hours in the month were DST ends", () => {
const item = {
@@ -79,6 +179,38 @@ describe("processWorkingHours", () => {
expect(allDSTStartAt12).toBeTruthy();
expect(allNotDSTStartAt13).toBeTruthy();
});
it("should skip event if it ends before it starts (different days)", () => {
const item = {
days: [1, 2, 3],
startTime: new Date(new Date().setUTCHours(8, 0, 0, 0)), // 8 AM
endTime: new Date(new Date().setUTCHours(7, 0, 0, 0)), // 7 AM
};
const timeZone = "America/New_York";
const dateFrom = dayjs("2023-11-07T00:00:00Z").tz(timeZone); // 2023-11-07T00:00:00 (America/New_York)
const dateTo = dayjs("2023-11-08T00:00:00Z").tz(timeZone); // 2023-11-08T00:00:00 (America/New_York)
const results = processWorkingHours({ item, timeZone, dateFrom, dateTo });
expect(results).toEqual([]);
});
it("should skip event if it ends before it starts (same day but different hours)", () => {
const item = {
days: [1],
startTime: new Date(new Date().setUTCHours(8, 0, 0, 0)), // 8 AM
endTime: new Date(new Date().setUTCHours(7, 0, 0, 0)), // 7 AM
};
const timeZone = "America/New_York";
const dateFrom = dayjs("2023-11-07T00:00:00Z").tz(timeZone); // 2023-11-07T00:00:00 (America/New_York)
const dateTo = dayjs("2023-11-07T23:59:59Z").tz(timeZone); // 2023-11-07T23:59:59 (America/New_York)
const results = processWorkingHours({ item, timeZone, dateFrom, dateTo });
expect(results).toEqual([]);
});
});
describe("processDateOverrides", () => {
+6 -2
View File
@@ -23,7 +23,7 @@ export function processWorkingHours({
}) {
const results = [];
for (let date = dateFrom.tz(timeZone).startOf("day"); dateTo.isAfter(date); date = date.add(1, "day")) {
const fromOffset = dateFrom.tz(timeZone).utcOffset();
const fromOffset = dateFrom.tz(timeZone).startOf("day").utcOffset();
const offset = date.tz(timeZone).utcOffset();
// it always has to be start of the day (midnight) even when DST changes
@@ -47,7 +47,11 @@ export function processWorkingHours({
const startResult = dayjs.max(start, dateFrom.tz(timeZone));
const endResult = dayjs.min(end, dateTo.tz(timeZone));
if (startResult.isAfter(endResult)) {
const isLater =
startResult.isAfter(endResult, "hour") ||
(startResult.isSame(endResult, "hour") && startResult.isAfter(endResult, "minute"));
if (isLater) {
// if an event ends before start, it's not a result.
continue;
}
@@ -0,0 +1,63 @@
import { renderHook } from "@testing-library/react-hooks";
import { vi } from "vitest";
import { describe, expect, it } from "vitest";
import { useParamsWithFallback } from "./useParamsWithFallback";
describe("useParamsWithFallback hook", () => {
it("should return router.query when param is null", () => {
vi.mock("next/navigation", () => ({
useParams: vi.fn().mockReturnValue(null),
}));
vi.mock("next/compat/router", () => ({
useRouter: vi.fn().mockReturnValue({ query: { id: 1 } }),
}));
const { result } = renderHook(() => useParamsWithFallback());
expect(result.current).toEqual({ id: 1 });
});
it("should return router.query when param is undefined", () => {
vi.mock("next/navigation", () => ({
useParams: vi.fn().mockReturnValue(undefined),
}));
vi.mock("next/compat/router", () => ({
useRouter: vi.fn().mockReturnValue({ query: { id: 1 } }),
}));
const { result } = renderHook(() => useParamsWithFallback());
expect(result.current).toEqual({ id: 1 });
});
it("should return useParams() if it exists", () => {
vi.mock("next/navigation", () => ({
useParams: vi.fn().mockReturnValue({ id: 1 }),
}));
vi.mock("next/compat/router", () => ({
useRouter: vi.fn().mockReturnValue(null),
}));
const { result } = renderHook(() => useParamsWithFallback());
expect(result.current).toEqual({ id: 1 });
});
it("should return useParams() if it exists", () => {
vi.mock("next/navigation", () => ({
useParams: vi.fn().mockReturnValue({ id: 1 }),
}));
vi.mock("next/compat/router", () => ({
useRouter: vi.fn().mockReturnValue({ query: { id: 2 } }),
}));
const { result } = renderHook(() => useParamsWithFallback());
expect(result.current).toEqual({ id: 1 });
});
});
+10 -5
View File
@@ -1,13 +1,18 @@
"use client";
import { useRouter as useCompatRouter } from "next/compat/router";
import { useParams } from "next/navigation";
import { useRouter } from "next/router";
import type { ParsedUrlQuery } from "querystring";
interface Params {
[key: string]: string | string[];
}
/**
* This hook is a workaround until pages are migrated to app directory.
*/
export function useParamsWithFallback() {
const router = useRouter();
const params = useParams();
return params || router.query;
export function useParamsWithFallback(): Params | ParsedUrlQuery {
const params = useParams(); // always `null` in pages router
const router = useCompatRouter(); // always `null` in app router
return params ?? router?.query ?? {};
}
+18 -6
View File
@@ -9,7 +9,12 @@ import { parseZone } from "./parse-zone";
type ExtraOptions = { withDefaultTimeFormat?: boolean; selectedTimeFormat?: TimeFormat };
const processDate = (date: string | null | Dayjs, language: string, options?: ExtraOptions) => {
const processDate = (
date: string | null | Dayjs,
language: string,
timeZone: string,
options?: ExtraOptions
) => {
const parsedZone = parseZone(date);
if (!parsedZone?.isValid()) return "Invalid date";
const formattedTime = parsedZone?.format(
@@ -17,12 +22,19 @@ const processDate = (date: string | null | Dayjs, language: string, options?: Ex
? TimeFormat.TWELVE_HOUR
: options?.selectedTimeFormat || detectBrowserTimeFormat
);
return `${formattedTime}, ${dayjs(date).toDate().toLocaleString(language, { dateStyle: "full" })}`;
return `${formattedTime}, ${dayjs(date)
.toDate()
.toLocaleString(language, { dateStyle: "full", timeZone })}`;
};
export const parseDate = (date: string | null | Dayjs, language: string, options?: ExtraOptions) => {
export const parseDate = (
date: string | null | Dayjs,
language: string,
timeZone: string,
options?: ExtraOptions
) => {
if (!date) return ["No date"];
return processDate(date, language, options);
return processDate(date, language, timeZone, options);
};
const timeOptions: Intl.DateTimeFormatOptions = {
@@ -74,7 +86,7 @@ export const parseRecurringDates = (
withDefaultTimeFormat,
}: {
startDate: string | null | Dayjs;
timeZone?: string;
timeZone: string;
recurringEvent: RecurringEvent | null;
recurringCount: number;
selectedTimeFormat?: TimeFormat;
@@ -98,7 +110,7 @@ export const parseRecurringDates = (
});
const dateStrings = times.map((t) => {
// finally; show in local timeZone again
return processDate(t.tz(timeZone), language, { selectedTimeFormat, withDefaultTimeFormat });
return processDate(t.tz(timeZone), language, timeZone, { selectedTimeFormat, withDefaultTimeFormat });
});
return [dateStrings, times.map((t) => t.toDate())];
+9 -4
View File
@@ -11,14 +11,15 @@ import { parseBookingLimit } from "../isBookingLimits";
export async function checkBookingLimits(
bookingLimits: IntervalLimit,
eventStartDate: Date,
eventId: number
eventId: number,
timeZone?: string | null
) {
const parsedBookingLimits = parseBookingLimit(bookingLimits);
if (!parsedBookingLimits) return false;
// not iterating entries to preserve types
const limitCalculations = ascendingLimitKeys.map((key) =>
checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId })
checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId, timeZone })
);
try {
@@ -33,19 +34,23 @@ export async function checkBookingLimit({
eventId,
key,
limitingNumber,
timeZone,
}: {
eventStartDate: Date;
eventId: number;
key: keyof IntervalLimit;
limitingNumber: number | undefined;
timeZone?: string | null;
}) {
{
const eventDateInOrganizerTz = timeZone ? dayjs(eventStartDate).tz(timeZone) : dayjs(eventStartDate);
if (!limitingNumber) return;
const unit = intervalLimitKeyToUnit(key);
const startDate = dayjs(eventStartDate).startOf(unit).toDate();
const endDate = dayjs(eventStartDate).endOf(unit).toDate();
const startDate = dayjs(eventDateInOrganizerTz).startOf(unit).toDate();
const endDate = dayjs(eventDateInOrganizerTz).endOf(unit).toDate();
const bookingsInPeriod = await prisma.booking.count({
where: {
+3 -1
View File
@@ -13,7 +13,9 @@ export function defaultResponder<T>(f: Handle<T>) {
performance.mark("Start");
const result = await f(req, res);
ok = true;
if (result) res.json(result);
if (result && !res.writableEnded) {
res.json(result);
}
} catch (err) {
console.error(err);
const error = getServerErrorFromUnknown(err);
+5 -1
View File
@@ -205,7 +205,11 @@ export const stringOrNumber = z.union([
z.number().int(),
]);
export const stringToDayjs = z.string().transform((val) => dayjs(val));
export const stringToDayjs = z.string().transform((val) => {
const matches = val.match(/([+-]\d{2}:\d{2})$/);
const timezone = matches ? matches[1] : "+00:00";
return dayjs(val).utcOffset(timezone);
});
export const bookingCreateBodySchema = z.object({
end: z.string().optional(),
@@ -1,3 +1,5 @@
import * as Sentry from "@sentry/nextjs";
import { redactError } from "@calcom/lib/redactError";
import { middleware } from "../trpc";
@@ -9,6 +11,7 @@ const captureErrorsMiddleware = middleware(async ({ next }) => {
if (!cause) {
return result;
}
Sentry.captureException(cause);
throw redactError(cause);
}
return result;
@@ -98,7 +98,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
sendTo = invitee.email;
}
// inform user of membership by email
if (input.sendEmailInvitation && ctx?.user?.name && team?.name) {
if (ctx?.user?.name && team?.name) {
const inviteTeamOptions = {
joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`,
isCalcomMember: true,
@@ -12,7 +12,6 @@ export const ZInviteMemberInputSchema = z.object({
}),
role: z.nativeEnum(MembershipRole),
language: z.string(),
sendEmailInvitation: z.boolean(),
isOrg: z.boolean().default(false),
});
@@ -243,7 +243,6 @@ export async function sendVerificationEmail({
role: "ADMIN" | "MEMBER" | "OWNER";
usernameOrEmail: string | string[];
language: string;
sendEmailInvitation: boolean;
isOrg: boolean;
};
connectionInfo: ReturnType<typeof getOrgConnectionInfo>;
@@ -23,7 +23,8 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
const isOrgAdmin = ctx.user.organizationId
? await isTeamAdmin(ctx.user.id, ctx.user.organizationId)
: false;
if (!isAdmin && ctx.user.id !== input.memberId) throw new TRPCError({ code: "UNAUTHORIZED" });
if (!(isAdmin || isOrgAdmin) && ctx.user.id !== input.memberId)
throw new TRPCError({ code: "UNAUTHORIZED" });
// Only a team owner can remove another team owner.
if ((await isTeamOwner(input.memberId, input.teamId)) && !(await isTeamOwner(ctx.user.id, input.teamId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
+1
View File
@@ -26,6 +26,7 @@
"outputs": [".next/**"],
"env": [
"NEXT_PUBLIC_IS_E2E",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY",
"NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRODUCT_ID",
"NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
+8 -1
View File
@@ -36,7 +36,7 @@ const workspaces = packagedEmbedTestsOnly
test: {
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
// TODO: Ignore the api until tests are fixed
exclude: ["**/node_modules/**/*", "packages/embeds/**/*"],
exclude: ["**/node_modules/**/*", "packages/embeds/**/*", "packages/lib/hooks/**/*"],
setupFiles: ["setupVitest.ts"],
},
},
@@ -67,6 +67,13 @@ const workspaces = packagedEmbedTestsOnly
setupFiles: ["packages/app-store/test-setup.ts"],
},
},
{
test: {
name: "@calcom/packages/lib/hooks",
include: ["packages/lib/hooks/**/*.{test,spec}.{ts,js}"],
environment: "jsdom",
},
},
];
export default defineWorkspace(workspaces);
+114 -115
View File
@@ -3431,7 +3431,7 @@ __metadata:
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@sentry/nextjs": ^7.20.0
"@sentry/nextjs": ^7.73.0
bcryptjs: ^2.4.3
memory-cache: ^0.2.0
next: ^13.4.6
@@ -4515,6 +4515,7 @@ __metadata:
"@radix-ui/react-switch": ^1.0.0
"@radix-ui/react-toggle-group": ^1.0.0
"@radix-ui/react-tooltip": ^1.0.0
"@sentry/nextjs": ^7.73.0
"@stripe/react-stripe-js": ^1.10.0
"@stripe/stripe-js": ^1.35.0
"@tanstack/react-query": ^4.3.9
@@ -10498,29 +10499,27 @@ __metadata:
languageName: node
linkType: hard
"@sentry-internal/tracing@npm:7.53.1":
version: 7.53.1
resolution: "@sentry-internal/tracing@npm:7.53.1"
"@sentry-internal/tracing@npm:7.77.0":
version: 7.77.0
resolution: "@sentry-internal/tracing@npm:7.77.0"
dependencies:
"@sentry/core": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
tslib: ^1.9.3
checksum: 99fadf422e619faeea436a96b37088ba10850f8e77fca10d7fea6de0c3f1d60b0e8ec1da3b9230f4213fa25f044c99aba585562447ddca0a443d7cbd88c3b81d
"@sentry/core": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
checksum: dc6162db6d426212440abd66f9e37a4ce1849a79dd067158091702b273f3312ea131984c7aad58708745340b2a0ddb652b898c27dbf409d8691dbd4ee8a9ebde
languageName: node
linkType: hard
"@sentry/browser@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/browser@npm:7.53.1"
"@sentry/browser@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/browser@npm:7.77.0"
dependencies:
"@sentry-internal/tracing": 7.53.1
"@sentry/core": 7.53.1
"@sentry/replay": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
tslib: ^1.9.3
checksum: 6250949dfbef169669d9e932515f0d965f449ef82be5fb6b813ab170944cef033c758857a3412aa1a1023d77622f88d8599df8e4b0ea126479f8713bd4aa1838
"@sentry-internal/tracing": 7.77.0
"@sentry/core": 7.77.0
"@sentry/replay": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
checksum: b3c4ecbf27e8d940c42bc151fc39e9e3659889c0091e69a39f3b8f0e316caa9cdd17aa2c00b151ff9976eff459f023e69cb51f8efa96a840191afd4091cc7573
languageName: node
linkType: hard
@@ -10540,112 +10539,119 @@ __metadata:
languageName: node
linkType: hard
"@sentry/core@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/core@npm:7.53.1"
"@sentry/core@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/core@npm:7.77.0"
dependencies:
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
tslib: ^1.9.3
checksum: 5d42bc30a59cb4459eb99441b631e3f9daec5b2e98c24377aae0329127f8c474a9c32353a96e3f4765e3fb18370756e85f6b873cd26bd591e0040917b7fa9cf4
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
checksum: f76e83ba5da4710b87b5e03f747ac5b5f93c93814447d29e1863b6bd3734d87f3a8fb197060a9a8370430d1d47b5c8c33dc86acc7f7b212322630e280435c6d8
languageName: node
linkType: hard
"@sentry/integrations@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/integrations@npm:7.53.1"
"@sentry/integrations@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/integrations@npm:7.77.0"
dependencies:
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
"@sentry/core": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
localforage: ^1.8.1
tslib: ^1.9.3
checksum: 55fc930af665afa07f81591f8898c8c10518696d48ec6d9f0864fc05fa5ef754868644f50362bac15734dff93592397ec022d80ded8754b97d64009e63a30712
checksum: b34f1e0a5a28154698b0eb42c9a3c968c35cbe1ba0411df7b4b83008a58cb3db2a2c07eee5e7a6e55f8b6aae9c74c8433d0e97a3e6289c979b361c09ced4475a
languageName: node
linkType: hard
"@sentry/nextjs@npm:^7.20.0":
version: 7.53.1
resolution: "@sentry/nextjs@npm:7.53.1"
"@sentry/nextjs@npm:^7.73.0":
version: 7.77.0
resolution: "@sentry/nextjs@npm:7.77.0"
dependencies:
"@rollup/plugin-commonjs": 24.0.0
"@sentry/core": 7.53.1
"@sentry/integrations": 7.53.1
"@sentry/node": 7.53.1
"@sentry/react": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
"@sentry/core": 7.77.0
"@sentry/integrations": 7.77.0
"@sentry/node": 7.77.0
"@sentry/react": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
"@sentry/vercel-edge": 7.77.0
"@sentry/webpack-plugin": 1.20.0
chalk: 3.0.0
resolve: 1.22.8
rollup: 2.78.0
stacktrace-parser: ^0.1.10
tslib: ^1.9.3
peerDependencies:
next: ^10.0.8 || ^11.0 || ^12.0 || ^13.0
next: ^10.0.8 || ^11.0 || ^12.0 || ^13.0 || ^14.0
react: 16.x || 17.x || 18.x
webpack: ">= 4.0.0"
peerDependenciesMeta:
webpack:
optional: true
checksum: 3b23a986b99b69453426f32f851eb3140d7ed29f066d2bf2d2562b72b8ca786fffa0c7903f0ab6f9a83ec1defaa2f7ec58db62515f4b95bd436ce33c862644db
checksum: 293019ff3bc85f1f041f15f1836cb98587cbecf780c26d7758a470d382042c9aa9522408853afbe16a73945f0e4edb6d5b3d704e8b8501b9d90304a6a9492afe
languageName: node
linkType: hard
"@sentry/node@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/node@npm:7.53.1"
"@sentry/node@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/node@npm:7.77.0"
dependencies:
"@sentry-internal/tracing": 7.53.1
"@sentry/core": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
cookie: ^0.4.1
"@sentry-internal/tracing": 7.77.0
"@sentry/core": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
https-proxy-agent: ^5.0.0
lru_map: ^0.3.3
tslib: ^1.9.3
checksum: e1b3c342c75a2a5883d6f1a52bc975bd6fb06643d2c26fd302cc0cfd23ca09509374f8c3afbe16208d007dc862e58b96946125dfebd3ba5856aa791cff04cea2
checksum: 11034db59a547f3b35208f601fa4704dc7ef6e1f9730136562c1764de8f7b286b355b701711febb44b972ed7009b3467c21ffda15f07024f7dee36979ca9e1c1
languageName: node
linkType: hard
"@sentry/react@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/react@npm:7.53.1"
"@sentry/react@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/react@npm:7.77.0"
dependencies:
"@sentry/browser": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
"@sentry/browser": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
hoist-non-react-statics: ^3.3.2
tslib: ^1.9.3
peerDependencies:
react: 15.x || 16.x || 17.x || 18.x
checksum: ee08c6a851cae421a134d62e217c192a961930254ea8775e1960420307f4ff08b659405bd883b12698a55085ad1070ac58e618f5799a7edc775368382e807e2d
checksum: 18af780c76b6b0c52ea476963b8ef0b7a346796c5d0e04dd7c4cb73eb3e52b2fd00b8c1cd5529c29983398a57e325d0d45a689bc8b9822d7c9b10339870869f9
languageName: node
linkType: hard
"@sentry/replay@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/replay@npm:7.53.1"
"@sentry/replay@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/replay@npm:7.77.0"
dependencies:
"@sentry/core": 7.53.1
"@sentry/types": 7.53.1
"@sentry/utils": 7.53.1
checksum: c3757077a971183f5f9f87006449a110c0f951c76a777aa45cb2f057100ec5bc8fcf50e4c58e8117ddc0f44fdaada101384a45b2b5c427048b4ce57eb780711d
"@sentry-internal/tracing": 7.77.0
"@sentry/core": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
checksum: 95bbd554a33a17273501fffadd7a51050787ef5a0ec26de5cccba35f6bfd5d88b2c13f735beaf74274cdb5f77d3a310fa2699d9629220de4b79e4d4c27a14e20
languageName: node
linkType: hard
"@sentry/types@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/types@npm:7.53.1"
checksum: 5f76d7d66d5df5d48f66a755e18ae133ea02a9405093b652eb4921ca62c3114343561a2d8067f2be6d9df8dd32d10e30d3ce58150f795600680e6230fb136681
"@sentry/types@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/types@npm:7.77.0"
checksum: ff8047425d4b0caae9221a9fa52fa0122ddf23b51152b7d71914f6465cf72dc3fdf00b7db16e3e95b4a1d2d429824d91ed586b1abac6d5286bd7800504e30824
languageName: node
linkType: hard
"@sentry/utils@npm:7.53.1":
version: 7.53.1
resolution: "@sentry/utils@npm:7.53.1"
"@sentry/utils@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/utils@npm:7.77.0"
dependencies:
"@sentry/types": 7.53.1
tslib: ^1.9.3
checksum: d9556f161a0eed0e1bb279bac12b99492b6212d9b6cabebbcb2d0f196f99b96c319305ec347d8f06382e2dde5782d5ba20a00f762fbfd4e8699e9100ef921804
"@sentry/types": 7.77.0
checksum: 000bb1caec44812dc39a8026ecba78c14a8332f15cb78d2935c91a7ba8d61eab9553a791a4af528a113ca90b83db6e81791e3562b2559b76596accc69e4d57ad
languageName: node
linkType: hard
"@sentry/vercel-edge@npm:7.77.0":
version: 7.77.0
resolution: "@sentry/vercel-edge@npm:7.77.0"
dependencies:
"@sentry/core": 7.77.0
"@sentry/types": 7.77.0
"@sentry/utils": 7.77.0
checksum: 3c23c46af233fe3d50f48509fd8e89c24be54d79419e0368bbcc574b6f1d886ff182300e507bbe5fb587c1219ee9663eaa1591783ca0a7bbebdd85b8b35a07ad
languageName: node
linkType: hard
@@ -18555,7 +18561,7 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:0.4.2, cookie@npm:^0.4.1, cookie@npm:^0.4.2":
"cookie@npm:0.4.2, cookie@npm:^0.4.2":
version: 0.4.2
resolution: "cookie@npm:0.4.2"
checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b
@@ -28217,13 +28223,6 @@ __metadata:
languageName: node
linkType: hard
"lru_map@npm:^0.3.3":
version: 0.3.3
resolution: "lru_map@npm:0.3.3"
checksum: ca9dd43c65ed7a4f117c548028101c5b6855e10923ea9d1f635af53ad20c5868ff428c364d454a7b57fe391b89c704982275410c3c5099cca5aeee00d76e169a
languageName: node
linkType: hard
"ltgt@npm:^2.1.2":
version: 2.2.1
resolution: "ltgt@npm:2.2.1"
@@ -35232,6 +35231,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:1.22.8, resolve@npm:^1.22.2":
version: 1.22.8
resolution: "resolve@npm:1.22.8"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c
languageName: node
linkType: hard
"resolve@npm:^1.1.7, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.3.2":
version: 1.22.1
resolution: "resolve@npm:1.22.1"
@@ -35258,19 +35270,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.22.2":
version: 1.22.8
resolution: "resolve@npm:1.22.8"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c
languageName: node
linkType: hard
"resolve@npm:^2.0.0-next.3":
version: 2.0.0-next.3
resolution: "resolve@npm:2.0.0-next.3"
@@ -35294,6 +35293,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@1.22.8#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.2#~builtin<compat/resolve>":
version: 1.22.8
resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847
languageName: node
linkType: hard
"resolve@patch:resolve@^1.1.7#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.17.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.3.2#~builtin<compat/resolve>":
version: 1.22.1
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=c3c19d"
@@ -35320,19 +35332,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@^1.22.2#~builtin<compat/resolve>":
version: 1.22.8
resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847
languageName: node
linkType: hard
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
version: 2.0.0-next.3
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.3#~builtin<compat/resolve>::version=2.0.0-next.3&hash=c3c19d"