Merge branch 'main' into testE2E-timezone

This commit is contained in:
GitStart-Cal.com 2023-11-07 19:14:23 +05:45 committed by GitHub
commit cb5a56a840
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 815 additions and 211 deletions

View File

@ -0,0 +1,4 @@
export function isValidBase64Image(input: string): boolean {
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
return regex.test(input);
}

View File

@ -4,6 +4,7 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
import { timeZone } from "~/lib/validations/shared/timeZone";
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
@ -106,6 +107,7 @@ const schemaUserEditParams = z.object({
.optional()
.nullable(),
locale: z.nativeEnum(locales).optional().nullable(),
avatar: z.string().refine(isValidBase64Image).optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
@ -128,6 +130,7 @@ const schemaUserCreateParams = z.object({
.nullable(),
locale: z.nativeEnum(locales).optional(),
createdDate: iso8601.optional(),
avatar: z.string().refine(isValidBase64Image).optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,

View File

@ -13,6 +13,7 @@
"lint": "eslint . --ignore-path .gitignore",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "PORT=3002 next start",
"docker-start-api": "PORT=80 next start",
"type-check": "tsc --pretty --noEmit"
},
"devDependencies": {

View File

@ -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

View File

@ -60,6 +60,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
* locale:
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
* type: string
* avatar:
* description: The user's avatar, in base64 format
* type: string
* examples:
* user:
* summary: An example of USER

View File

@ -631,8 +631,7 @@ paths:
type: string
role:
type: string
sendEmailInvitation:
type: boolean
parameters:
- schema:
type: string

View File

@ -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);
});
});
});
});

View File

@ -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");

3
infra/README.md Normal file
View File

@ -0,0 +1,3 @@
## Infrastructure Folder
This folder, located within the "infra" directory of our monorepo, is dedicated to managing the infrastructure as code (IaC) and Docker-related files for our project. It plays a critical role in orchestrating, configuring, and maintaining the infrastructure that our applications rely on.

View File

@ -0,0 +1,60 @@
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.7.0
FROM node:${NODE_VERSION}-slim as base
# Next.js/Prisma app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Throw-away build stage to reduce size of final image
FROM base as build
# copy all required files from the monorepo
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json git-init.sh git-setup.sh ./
COPY /.yarn ./.yarn
COPY ./apps/api ./apps/api
COPY ./packages ./packages
# TODO: follow up pr to remove dependencies on web
COPY ./apps/web ./apps/web
# Install node modules and dependencies, prune unneeded deps, then build
RUN set -eux; \
apt-get update -qq && \
apt-get install -y build-essential openssl pkg-config python-is-python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
yarn config set httpTimeout 1200000 && \
npx turbo prune --scope=@calcom/web --docker && \
npx turbo prune --scope=@calcom/api --docker && \
yarn install && \
yarn turbo run build --filter=@calcom/api
# Final stage
FROM base
WORKDIR /app
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y openssl && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built application
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
COPY --from=build /app/apps/api/.next ./apps/api/.next
COPY --from=build /app/apps/api/.turbo ./apps/api/.turbo
COPY --from=build /app/turbo.json ./turbo.json
COPY --from=build /app/yarn.lock ./yarn.lock
# Expose port 80
EXPOSE 80
# Start cmd, called when docker image is mounted
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]

View File

@ -71,7 +71,10 @@
"test": "vitest run",
"type-check": "turbo run type-check",
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
"web": "yarn workspace @calcom/web"
"web": "yarn workspace @calcom/web",
"docker-build-api": "docker build -t cal-api -f ./infra/docker/api/Dockerfile .",
"docker-run-api": "docker run -p 80:80 cal-api",
"docker-stop-api": "docker ps --filter 'ancestor=cal-api' -q | xargs docker stop"
},
"devDependencies": {
"@changesets/cli": "^2.26.1",

View File

@ -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,
});
}}>

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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

View File

@ -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) => {

View File

@ -205,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) => {

View File

@ -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,

View File

@ -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;
}

View File

@ -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,
});
}}

View File

@ -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,

View File

@ -12,7 +12,6 @@ export const ZInviteMemberInputSchema = z.object({
}),
role: z.nativeEnum(MembershipRole),
language: z.string(),
sendEmailInvitation: z.boolean(),
isOrg: z.boolean().default(false),
});

View File

@ -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>;