Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8abbb3e9 | ||
|
|
7bb42ba577 | ||
|
|
2e3b3257db | ||
|
|
d28e20e92e | ||
|
|
8b89d6635a | ||
|
|
13ec810cb9 | ||
|
|
367c8a96d8 | ||
|
|
e94c030c45 | ||
|
|
6d0c7e4214 | ||
|
|
e11cb9e3a5 | ||
|
|
809abd2f4f | ||
|
|
d1d50b0d91 | ||
|
|
fc716f5921 | ||
|
|
5348464972 | ||
|
|
3fac6e698e | ||
|
|
dcafbf672a | ||
|
|
dff89a4df8 | ||
|
|
ed04062540 | ||
|
|
23fda4a9e6 | ||
|
|
14c3028c2a | ||
|
|
dfe49e1732 | ||
|
|
6148c6baea | ||
|
|
5366d891bb | ||
|
|
2ee021a87f | ||
|
|
dd0c2dbff2 | ||
|
|
f044c2d7c7 | ||
|
|
703bcd861e | ||
|
|
d4d96eb5cd | ||
|
|
d8356e9a5b | ||
|
|
9348279818 | ||
|
|
5d77e4ce1d | ||
|
|
968e782dbe | ||
|
|
01fb3dd03b | ||
|
|
c3537f3290 | ||
|
|
3bb42f276e |
+5
-1
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 0,
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import prismock from "../../../../tests/libs/__mocks__/prisma";
|
||||
import { diff } from "jest-diff";
|
||||
import { 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?
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## Infrastructure Folder
|
||||
This folder, located within the "infra" directory of our monorepo, is dedicated to managing the infrastructure as code (IaC) and Docker-related files for our project. It plays a critical role in orchestrating, configuring, and maintaining the infrastructure that our applications rely on.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Adjust NODE_VERSION as desired
|
||||
ARG NODE_VERSION=20.7.0
|
||||
FROM node:${NODE_VERSION}-slim as base
|
||||
|
||||
# Next.js/Prisma app lives here
|
||||
WORKDIR /app
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base as build
|
||||
|
||||
# copy all required files from the monorepo
|
||||
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json git-init.sh git-setup.sh ./
|
||||
COPY /.yarn ./.yarn
|
||||
COPY ./apps/api ./apps/api
|
||||
COPY ./packages ./packages
|
||||
# TODO: follow up pr to remove dependencies on web
|
||||
COPY ./apps/web ./apps/web
|
||||
|
||||
# Install node modules and dependencies, prune unneeded deps, then build
|
||||
RUN set -eux; \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y build-essential openssl pkg-config python-is-python3 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||
yarn config set httpTimeout 1200000 && \
|
||||
npx turbo prune --scope=@calcom/web --docker && \
|
||||
npx turbo prune --scope=@calcom/api --docker && \
|
||||
yarn install && \
|
||||
yarn turbo run build --filter=@calcom/api
|
||||
|
||||
|
||||
# Final stage
|
||||
FROM base
|
||||
WORKDIR /app
|
||||
|
||||
# Install packages needed for deployment
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y openssl && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Copy built application
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
|
||||
COPY --from=build /app/apps/api/.next ./apps/api/.next
|
||||
COPY --from=build /app/apps/api/.turbo ./apps/api/.turbo
|
||||
COPY --from=build /app/turbo.json ./turbo.json
|
||||
COPY --from=build /app/yarn.lock ./yarn.lock
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start cmd, called when docker image is mounted
|
||||
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
|
||||
+4
-1
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
|
||||
export function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
||||
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
|
||||
const mergedDateRanges: DateRange[] = [];
|
||||
|
||||
let currentRange = dateRanges[0];
|
||||
if (!currentRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let i = 1; i < dateRanges.length; i++) {
|
||||
const nextRange = dateRanges[i];
|
||||
|
||||
if (isCurrentRangeOverlappingNext(currentRange, nextRange)) {
|
||||
currentRange = {
|
||||
start: currentRange.start,
|
||||
end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
|
||||
};
|
||||
} else {
|
||||
mergedDateRanges.push(currentRange);
|
||||
currentRange = nextRange;
|
||||
}
|
||||
}
|
||||
mergedDateRanges.push(currentRange);
|
||||
|
||||
return mergedDateRanges;
|
||||
}
|
||||
|
||||
function isCurrentRangeOverlappingNext(currentRange: DateRange, nextRange: DateRange): boolean {
|
||||
return (
|
||||
currentRange.start.valueOf() <= nextRange.start.valueOf() &&
|
||||
currentRange.end.valueOf() > nextRange.start.valueOf()
|
||||
);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
|
||||
import { mergeOverlappingDateRanges } from ".";
|
||||
|
||||
const november2 = "2023-11-02";
|
||||
const november3 = "2023-11-03";
|
||||
|
||||
describe("mergeOverlappingDateRanges", () => {
|
||||
it("should merge all ranges into one when one range includes all others", () => {
|
||||
const dateRanges = [
|
||||
createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`), // Includes all others
|
||||
createDateRange(`${november2}T23:15:00.000Z`, `${november3}T00:00:00.000Z`),
|
||||
createDateRange(`${november3}T00:15:00.000Z`, `${november3}T01:00:00.000Z`),
|
||||
createDateRange(`${november3}T01:15:00.000Z`, `${november3}T02:00:00.000Z`),
|
||||
];
|
||||
|
||||
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||
expect(mergedRanges).toHaveLength(1);
|
||||
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||
});
|
||||
|
||||
it("should merge only overlapping ranges over 2 days and leave non-overlapping ranges as is", () => {
|
||||
const dateRanges = [
|
||||
createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`),
|
||||
createDateRange(`${november3}T05:00:00.000Z`, `${november3}T06:00:00.000Z`),
|
||||
createDateRange(`${november3}T08:00:00.000Z`, `${november3}T10:00:00.000Z`), // This range should not be merged
|
||||
];
|
||||
|
||||
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||
expect(mergedRanges).toHaveLength(2);
|
||||
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||
expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true);
|
||||
expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true);
|
||||
});
|
||||
|
||||
it("should merge ranges that overlap on the same day", () => {
|
||||
const dateRanges = [
|
||||
createDateRange(`${november2}T01:00:00.000Z`, `${november2}T04:00:00.000Z`),
|
||||
createDateRange(`${november2}T02:00:00.000Z`, `${november2}T03:00:00.000Z`), // This overlaps with the first range
|
||||
createDateRange(`${november2}T05:00:00.000Z`, `${november2}T06:00:00.000Z`), // This doesn't overlap with above
|
||||
];
|
||||
|
||||
const mergedRanges = mergeOverlappingDateRanges(dateRanges);
|
||||
expect(mergedRanges).toHaveLength(2);
|
||||
expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true);
|
||||
expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true);
|
||||
expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true);
|
||||
expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function createDateRange(start: string, end: string): DateRange {
|
||||
return {
|
||||
start: dayjs(start),
|
||||
end: dayjs(end),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
import { intersect } from "@calcom/lib/date-ranges";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
|
||||
|
||||
export const getAggregatedAvailability = (
|
||||
userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[],
|
||||
schedulingType: SchedulingType | null
|
||||
): DateRange[] => {
|
||||
const fixedHosts = userAvailability.filter(
|
||||
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
|
||||
);
|
||||
|
||||
const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges);
|
||||
|
||||
const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
|
||||
if (unfixedHosts.length) {
|
||||
dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges));
|
||||
}
|
||||
|
||||
const availability = intersect(dateRangesToIntersect);
|
||||
|
||||
return mergeOverlappingDateRanges(availability);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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 ?? {};
|
||||
}
|
||||
|
||||
@@ -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())];
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user