Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
844592e15e | ||
|
|
bdacb18d5c | ||
|
|
1b5d50c45c | ||
|
|
1c39dbad3d | ||
|
|
e60454750e | ||
|
|
286f696302 | ||
|
|
8deaea3edd | ||
|
|
7d497a621f | ||
|
|
5c8e0483b1 | ||
|
|
3cefa94a94 | ||
|
|
30549d5c14 | ||
|
|
b87972399e | ||
|
|
0784df6a31 | ||
|
|
fc062878ed | ||
|
|
33c1d80a42 | ||
|
|
1064e1181d | ||
|
|
dfd683ffc4 | ||
|
|
0972307998 | ||
|
|
157f633ef2 | ||
|
|
cc1c3b210a | ||
|
|
fcfd47e7d2 | ||
|
|
284c4c9084 | ||
|
|
9fd3594f6c | ||
|
|
531371bf55 | ||
|
|
99fd4fe7a4 | ||
|
|
a677d0d2c4 | ||
|
|
d8cee20d0c | ||
|
|
a7ce12e22f | ||
|
|
88b67a8121 | ||
|
|
7e2a1a23b9 | ||
|
|
a115a3458e | ||
|
|
ae503acccc | ||
|
|
61e7c89f36 | ||
|
|
60ba7a5ca2 | ||
|
|
bfa718e193 | ||
|
|
b29a52d0b0 | ||
|
|
c21d9d08ab | ||
|
|
615e1dcfe8 | ||
|
|
a24f9b76f6 | ||
|
|
62f8ac1c6d | ||
|
|
09b91d5a7e | ||
|
|
01ac2e640a | ||
|
|
e39bf37f91 | ||
|
|
1f845944ac | ||
|
|
7f53db601d | ||
|
|
1cc9f1d25d | ||
|
|
c6ee482478 | ||
|
|
5292dd2e66 | ||
|
|
f59d62f03c | ||
|
|
cb02268e7f | ||
|
|
e33453c50d | ||
|
|
15ffeec745 | ||
|
|
b7252ed79c | ||
|
|
9b04df9bbe | ||
|
|
657be16a4e | ||
|
|
b0afb15939 | ||
|
|
3a9b8a9bf6 | ||
|
|
92a919069d | ||
|
|
0071d16575 | ||
|
|
23d22ee7ea | ||
|
|
3a4f944aac | ||
|
|
3eadae0aad | ||
|
|
0cc2fe2d7f | ||
|
|
0970c77009 | ||
|
|
5b733765f4 | ||
|
|
83fe4d6625 | ||
|
|
91beeeb525 | ||
|
|
81c0b1ffe9 | ||
|
|
aedc88f975 | ||
|
|
67256b5b6e | ||
|
|
ef02d64f90 | ||
|
|
8685862617 | ||
|
|
7422488b5d | ||
|
|
c3e1481d6a | ||
|
|
a1d1883b34 | ||
|
|
f2ae94a190 | ||
|
|
b82a597645 | ||
|
|
5ba0ee8e85 | ||
|
|
1463f97f14 | ||
|
|
34fea35e83 | ||
|
|
2d75048037 | ||
|
|
b549735334 | ||
|
|
86c379817e | ||
|
|
5d1976295c | ||
|
|
e9b1fa1f50 | ||
|
|
80080dda7a | ||
|
|
27c25e1f95 | ||
|
|
858be0f5d8 | ||
|
|
68d0090f50 | ||
|
|
a43fa13b11 | ||
|
|
2bf6da30b3 | ||
|
|
d85e0b51e4 | ||
|
|
55a317d677 | ||
|
|
5fff8fc58c | ||
|
|
0d68767d0a | ||
|
|
2051e87436 | ||
|
|
77a157a12b | ||
|
|
c4f25f2c44 | ||
|
|
d967facb63 | ||
|
|
c6dad39fb0 | ||
|
|
eec7f4d7d4 | ||
|
|
95d5824ba9 | ||
|
|
5f95a4d041 | ||
|
|
8e97444129 | ||
|
|
8d8264b5f5 | ||
|
|
2ad81221d0 | ||
|
|
3d1ac4523f | ||
|
|
b38807cb0f | ||
|
|
11fb387370 | ||
|
|
1546f9115d | ||
|
|
40ecd5dd9d | ||
|
|
ac6ac14a23 | ||
|
|
c6534160e4 | ||
|
|
aa2db562b8 | ||
|
|
b7ed314be2 | ||
|
|
d102268b33 | ||
|
|
f4397d6593 | ||
|
|
192603a1fa | ||
|
|
a3d969680f | ||
|
|
adc3b3aec6 | ||
|
|
92286e2453 | ||
|
|
51b2a15cb0 | ||
|
|
fa9a099dd6 | ||
|
|
83ceb46cca | ||
|
|
75d5576e17 | ||
|
|
0a6dde4fcc | ||
|
|
73b5839284 | ||
|
|
1a42b94b13 | ||
|
|
acb910f680 | ||
|
|
38e857f9a9 | ||
|
|
b8149dee36 | ||
|
|
f583ef40f2 | ||
|
|
71574488c5 | ||
|
|
008e0e43a2 | ||
|
|
da22eda662 | ||
|
|
f94d696885 | ||
|
|
9e84eb1b2c | ||
|
|
4c549811a1 | ||
|
|
05502f0cfb | ||
|
|
e436b8e06b | ||
|
|
8146cf8c93 | ||
|
|
c05f595ef0 | ||
|
|
0a795f5363 | ||
|
|
5b50a469b2 | ||
|
|
c08365f09c | ||
|
|
477e102388 | ||
|
|
e1b10b4ec5 | ||
|
|
ff6db3c572 | ||
|
|
4fa3fdef97 | ||
|
|
f29f6632e8 | ||
|
|
36747988e6 | ||
|
|
3dd312b88b | ||
|
|
3c9e9329b0 | ||
|
|
8617f43a8e | ||
|
|
5f0eb652b0 | ||
|
|
26d7301885 | ||
|
|
ffc4d22ff1 | ||
|
|
ee286a600f | ||
|
|
72c493ee21 | ||
|
|
5bd0428d18 | ||
|
|
c7089b9db6 | ||
|
|
81ad00a816 | ||
|
|
b5730da56d | ||
|
|
9455d14ad5 | ||
|
|
a056160404 | ||
|
|
6d53cd84f8 | ||
|
|
30cb8f745d | ||
|
|
bafa3c97ac | ||
|
|
bb48ccbad9 | ||
|
|
0a68c6d048 | ||
|
|
bb9f8e5d38 | ||
|
|
c99bb190f1 | ||
|
|
f0d2a45fdf | ||
|
|
e5f9315ee6 | ||
|
|
f7782b2080 | ||
|
|
e1f2dbab84 | ||
|
|
c2edbbcea7 | ||
|
|
c7e78cc181 | ||
|
|
7ba1557d1d | ||
|
|
d5b45b757a | ||
|
|
bb3b262bd7 | ||
|
|
d4f41e803a | ||
|
|
74eafa59e1 | ||
|
|
e6cd016cad | ||
|
|
7b0d5959cb | ||
|
|
7702ded85e | ||
|
|
5acb3e8f97 | ||
|
|
41046aaf27 | ||
|
|
7cffb30daa | ||
|
|
26bacb6c8e | ||
|
|
01b5c1f11e | ||
|
|
f5ba04599b | ||
|
|
994e602c8d | ||
|
|
a776c72d48 | ||
|
|
6adb5b413d | ||
|
|
b183a5779a | ||
|
|
16c1cf7b23 | ||
|
|
cf4ee142de | ||
|
|
a5d99bbec9 | ||
|
|
5d0468a986 | ||
|
|
7fef9db4ce | ||
|
|
296a367955 | ||
|
|
1ff2d51ee5 | ||
|
|
092cb0f691 | ||
|
|
0c74a98721 | ||
|
|
2082a4f4ed | ||
|
|
f06ef84f61 | ||
|
|
1080d7f88f | ||
|
|
4ebd52c9af | ||
|
|
e0ac95578c | ||
|
|
473012f2c0 | ||
|
|
1293588ac4 | ||
|
|
c0c40185fc | ||
|
|
3348e7be2f | ||
|
|
97dcb2a2a9 | ||
|
|
5dad78f8fe | ||
|
|
88cc83a0aa | ||
|
|
9dddf5e525 | ||
|
|
9df9f9bb6b | ||
|
|
c4803be323 | ||
|
|
34d360a4ec | ||
|
|
0e6f44e2b9 | ||
|
|
e377997d49 | ||
|
|
a0451e34c1 | ||
|
|
d2b4760e3a | ||
|
|
8469c91973 | ||
|
|
0990549e7f | ||
|
|
e884348807 | ||
|
|
5684ce2d28 | ||
|
|
bd8ac12f14 | ||
|
|
512628e8ab | ||
|
|
903a154dbc | ||
|
|
1a3d54a0d0 | ||
|
|
a5970cb1a8 | ||
|
|
925dc04f1c | ||
|
|
271f31730d | ||
|
|
684f4a795c | ||
|
|
836429348c | ||
|
|
a55138348f | ||
|
|
66b5dfd796 | ||
|
|
bba68e09a9 | ||
|
|
bab182960f | ||
|
|
f09153b3bb | ||
|
|
3e492eb17b |
+7
-4
@@ -23,6 +23,9 @@ CALCOM_LICENSE_KEY=
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
# Needed to run migrations while using a connection pooler like PgBouncer
|
||||
# Use the same one as DATABASE_URL if you're not using a connection pooler
|
||||
DATABASE_DIRECT_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
INSIGHTS_DATABASE_URL=
|
||||
@@ -134,8 +137,8 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Formbricks Experience Management Integration
|
||||
FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||
FORMBRICKS_ENVIRONMENT_ID=
|
||||
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
||||
FORMBRICKS_FEEDBACK_SURVEY_ID=
|
||||
|
||||
# AvatarAPI
|
||||
@@ -291,9 +294,9 @@ E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
CALCOM_WEBHOOK_SECRET=""
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
|
||||
@@ -18,6 +18,7 @@ runs:
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ inputs.DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Manually Delete BuildJet Cache
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cache_key:
|
||||
description: "BuildJet Cache Key to Delete"
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
manually-delete-buildjet-cache:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: buildjet/cache-delete@v1
|
||||
with:
|
||||
cache_key: ${{ inputs.cache_key }}
|
||||
@@ -11,6 +11,9 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
@@ -18,6 +21,9 @@ jobs:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
@@ -39,6 +45,7 @@ jobs:
|
||||
- uses: ./.github/actions/cache-db
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
@@ -50,6 +57,7 @@ jobs:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
|
||||
@@ -11,6 +11,9 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
@@ -41,6 +44,7 @@ jobs:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
|
||||
@@ -11,6 +11,9 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
@@ -18,6 +21,9 @@ jobs:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
@@ -44,6 +50,7 @@ jobs:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
|
||||
@@ -11,6 +11,9 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
@@ -18,6 +21,9 @@ jobs:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
@@ -42,6 +48,7 @@ jobs:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
|
||||
@@ -7,6 +7,7 @@ env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
@@ -41,6 +42,9 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
|
||||
+226
-56
@@ -1,9 +1,10 @@
|
||||
// This transform config was generated by Snaplet.
|
||||
// Snaplet found fields that may contain personally identifiable information (PII)
|
||||
// and used that to populate this file.
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path=".snaplet/snaplet.d.ts" />
|
||||
// This config was generated by Snaplet make sure to check it over before using it.
|
||||
import { copycat as c } from "@snaplet/copycat";
|
||||
import { defineConfig } from "snaplet";
|
||||
|
||||
import type { Transform } from "./structure";
|
||||
// c.setHashKey(REPLACE_ME_WITH_YOUR_HASH_KEY);
|
||||
|
||||
function hasStringProp<T extends string>(x: unknown, key: T): x is { [key in T]: string } {
|
||||
return !!x && typeof x === "object" && key in x;
|
||||
@@ -41,57 +42,226 @@ function replaceSensitiveKeys(record: object) {
|
||||
|
||||
const generateUsername = (x: string) => `${c.firstName(x)}-${c.lastName(x)}${c.int(x, { min: 2, max: 99 })}`;
|
||||
|
||||
const config: Transform = () => ({
|
||||
public: {
|
||||
ApiKey: ({ row }) => ({
|
||||
hashedKey: c.uuid(row.hashedKey),
|
||||
}),
|
||||
App: ({ row }) => ({
|
||||
keys: replaceSensitiveKeys(row.keys),
|
||||
}),
|
||||
Attendee: ({ row }) => ({
|
||||
email: c.email(row.email),
|
||||
name: c.fullName(row.name),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
}),
|
||||
Credential: ({ row }) => ({
|
||||
key: typeof row.key === "string" ? c.uuid(row.key) : replaceSensitiveKeys(row.key),
|
||||
}),
|
||||
EventType: ({ row }) => ({
|
||||
slug: generateSlug(row.slug),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
eventName: c.words(row.eventName, { max: 3 }),
|
||||
}),
|
||||
ResetPasswordRequest: ({ row }) => ({
|
||||
email: c.email(row.email),
|
||||
}),
|
||||
Schedule: ({ row }) => ({
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
}),
|
||||
Team: ({ row }) => ({
|
||||
bio: c.sentence(row.bio),
|
||||
name: c.words(row.name, { max: 2 }),
|
||||
slug: generateSlug(row.slug),
|
||||
}),
|
||||
users: ({ row }) =>
|
||||
row.role !== "ADMIN"
|
||||
? {
|
||||
bio: c.sentence(row.bio),
|
||||
email: c.email(row.email),
|
||||
name: c.fullName(row.name),
|
||||
password: c.password(row.password),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
username: generateUsername(row.username),
|
||||
}
|
||||
: row,
|
||||
VerificationToken: ({ row }) => ({
|
||||
token: c.uuid(row.token),
|
||||
}),
|
||||
Account: ({ row }) => ({
|
||||
access_token: c.uuid(row.access_token),
|
||||
refresh_token: c.uuid(row.refresh_token),
|
||||
}),
|
||||
export default defineConfig({
|
||||
transform: {
|
||||
$mode: "unsafe",
|
||||
public: {
|
||||
Account({ row }) {
|
||||
return {
|
||||
refresh_token: c.uuid(row.refresh_token),
|
||||
access_token: c.uuid(row.access_token),
|
||||
expires_at: c.int(row.expires_at, {
|
||||
min: 0,
|
||||
max: Math.pow(4, 8) - 1,
|
||||
}),
|
||||
token_type: c.uuid(row.token_type),
|
||||
id_token: c.uuid(row.id_token),
|
||||
};
|
||||
},
|
||||
ApiKey: ({ row }) => ({
|
||||
hashedKey: c.uuid(row.hashedKey),
|
||||
note: c.fullName(row.note),
|
||||
createdAt: c.dateString(row.createdAt, {
|
||||
minYear: 2020,
|
||||
}),
|
||||
}),
|
||||
App: ({ row }) => ({
|
||||
keys: replaceSensitiveKeys(row.keys),
|
||||
}),
|
||||
App_RoutingForms_Form({ row }) {
|
||||
return {
|
||||
name: c.fullName(row.name),
|
||||
};
|
||||
},
|
||||
Attendee: ({ row }) => ({
|
||||
email: c.email(row.email),
|
||||
name: c.fullName(row.name),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
locale: c.fullName(row.locale),
|
||||
}),
|
||||
Availability({ row }) {
|
||||
return {
|
||||
startTime: c
|
||||
.dateString(row.startTime, {
|
||||
minYear: 2020,
|
||||
})
|
||||
.slice(11, 19),
|
||||
endTime: c
|
||||
.dateString(row.endTime, {
|
||||
minYear: 2020,
|
||||
})
|
||||
.slice(11, 19),
|
||||
};
|
||||
},
|
||||
Booking({ row }) {
|
||||
return {
|
||||
title: c.fullName(row.title),
|
||||
startTime: c.dateString(row.startTime, {
|
||||
minYear: 2020,
|
||||
}),
|
||||
endTime: c.dateString(row.endTime, {
|
||||
minYear: 2020,
|
||||
}),
|
||||
location: c.sentence(row.location),
|
||||
metadata: {
|
||||
[c.word(row.metadata)]: c.words(row.metadata),
|
||||
},
|
||||
};
|
||||
},
|
||||
Credential: ({ row }) => ({
|
||||
key: typeof row.key === "string" ? c.uuid(row.key) : replaceSensitiveKeys(row.key),
|
||||
}),
|
||||
EventType: ({ row }) => ({
|
||||
slug: generateSlug(row.slug),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
eventName: c.words(row.eventName, { max: 3 }),
|
||||
currency: c.sentence(row.currency),
|
||||
}),
|
||||
EventTypeCustomInput({ row }) {
|
||||
return {
|
||||
label: c.fullName(row.label),
|
||||
};
|
||||
},
|
||||
Feature({ row }) {
|
||||
return {
|
||||
slug: c.uuid(row.slug),
|
||||
};
|
||||
},
|
||||
InstantMeetingToken({ row }) {
|
||||
return {
|
||||
token: c.uuid(row.token),
|
||||
};
|
||||
},
|
||||
OAuthClient({ row }) {
|
||||
return {
|
||||
name: c.fullName(row.name),
|
||||
};
|
||||
},
|
||||
OutOfOfficeEntry({ row }) {
|
||||
return {
|
||||
start: c.dateString(row.start, {
|
||||
minYear: 2020,
|
||||
}),
|
||||
};
|
||||
},
|
||||
Payment({ row }) {
|
||||
return {
|
||||
amount: c.int(row.amount, {
|
||||
min: 0,
|
||||
max: Math.pow(4, 8) - 1,
|
||||
}),
|
||||
currency: c.sentence(row.currency),
|
||||
data: {
|
||||
[c.word(row.data)]: c.words(row.data),
|
||||
},
|
||||
};
|
||||
},
|
||||
ResetPasswordRequest: ({ row }) => ({
|
||||
email: c.email(row.email),
|
||||
}),
|
||||
Schedule: ({ row }) => ({
|
||||
name: c.fullName(row.name),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
}),
|
||||
Session({ row }) {
|
||||
return {
|
||||
sessionToken: c.uuid(row.sessionToken),
|
||||
};
|
||||
},
|
||||
Team: ({ row }) => ({
|
||||
bio: c.sentence(row.bio),
|
||||
name: c.words(row.name, { max: 2 }),
|
||||
slug: generateSlug(row.slug),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
logoUrl: c.username(row.logoUrl),
|
||||
}),
|
||||
TempOrgRedirect({ row }) {
|
||||
return {
|
||||
from: c.dateString(row.from, {
|
||||
maxYear: 1999,
|
||||
}),
|
||||
toUrl: c.city(row.toUrl),
|
||||
};
|
||||
},
|
||||
VerificationToken({ row }) {
|
||||
return {
|
||||
id: c
|
||||
.int(row.id, {
|
||||
min: 1,
|
||||
max: Math.pow(4, 8) - 1,
|
||||
})
|
||||
.toString(),
|
||||
identifier: c.uuid(row.identifier),
|
||||
token: c.uuid(row.token),
|
||||
expires: c.dateString(row.expires, {
|
||||
minYear: 2020,
|
||||
}),
|
||||
};
|
||||
},
|
||||
VerifiedNumber({ row }) {
|
||||
return {
|
||||
phoneNumber: c.phoneNumber(row.phoneNumber),
|
||||
};
|
||||
},
|
||||
Webhook({ row }) {
|
||||
return {
|
||||
subscriberUrl: c.url(row.subscriberUrl),
|
||||
secret: c.streetAddress(row.secret),
|
||||
};
|
||||
},
|
||||
WebhookScheduledTriggers({ row }) {
|
||||
return {
|
||||
jobName: c.fullName(row.jobName),
|
||||
payload: c.password(row.payload),
|
||||
};
|
||||
},
|
||||
WorkflowStep({ row }) {
|
||||
return {
|
||||
sendTo: c.oneOf(row.sendTo, [
|
||||
"Man",
|
||||
"Woman",
|
||||
"Transgender",
|
||||
"Non-binary/non-conforming",
|
||||
"Not specified",
|
||||
]),
|
||||
sender: c.oneOf(row.sender, [
|
||||
"Man",
|
||||
"Woman",
|
||||
"Transgender",
|
||||
"Non-binary/non-conforming",
|
||||
"Not specified",
|
||||
]),
|
||||
};
|
||||
},
|
||||
users: ({ row }) =>
|
||||
row.role !== "ADMIN"
|
||||
? {
|
||||
bio: c.sentence(row.bio),
|
||||
email: c.email(row.email),
|
||||
name: c.fullName(row.name),
|
||||
password: c.password(row.password),
|
||||
timeZone: c.timezone(row.timeZone),
|
||||
username: generateUsername(row.username),
|
||||
metadata: {
|
||||
[c.word(row.metadata)]: c.words(row.metadata),
|
||||
},
|
||||
}
|
||||
: row,
|
||||
},
|
||||
},
|
||||
subset: {
|
||||
enabled: true,
|
||||
version: "3",
|
||||
targets: [
|
||||
{
|
||||
table: "public.users",
|
||||
rowLimit: 100,
|
||||
},
|
||||
],
|
||||
keepDisconnectedTables: false,
|
||||
followNullableRelations: true,
|
||||
maxCyclesLoop: 0,
|
||||
eager: false,
|
||||
taskSortAlgorithm: "children",
|
||||
},
|
||||
});
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -8,14 +8,14 @@ test.describe("Org", () => {
|
||||
const response = await page.goto("https://i.cal.com/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.screenshot({ path: "screenshot.jpg" });
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
@@ -23,14 +23,14 @@ test.describe("Org", () => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||
@@ -38,9 +38,10 @@ test.describe("Org", () => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Dynamic Group Booking", () => {
|
||||
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||
const users = [
|
||||
@@ -63,12 +64,39 @@ test.describe("Org", () => {
|
||||
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com")).toBeVisible();
|
||||
await expect(page.locator("text=Engineering")).toBeVisible();
|
||||
await expect(page.locator("text=Marketing")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Browse the Engineering Team", async () => {
|
||||
test("By User Navigation", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com");
|
||||
await page.click('text="Engineering"');
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
|
||||
test("By /team/engineering", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com/team/engineering");
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
|
||||
test("By /engineering", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com/engineering");
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
async function expectPageToBeServerSideRendered(page: Page) {
|
||||
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
|
||||
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"author": "Cal.com Inc.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"@langchain/core": "^0.1.26",
|
||||
"@langchain/openai": "^0.0.14",
|
||||
"@t3-oss/env-nextjs": "^0.6.1",
|
||||
"langchain": "^0.0.131",
|
||||
"langchain": "^0.1.17",
|
||||
"mailparser": "^3.6.5",
|
||||
"next": "^13.5.4",
|
||||
"supports-color": "8.1.1",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mailparser": "^3.4.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { UserList } from "~/src/types/user";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "~/src/env.mjs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DynamicStructuredTool } from "langchain/tools";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
|
||||
+64
-58
@@ -1,5 +1,6 @@
|
||||
import { initializeAgentExecutorWithOptions } from "langchain/agents";
|
||||
import { ChatOpenAI } from "langchain/chat_models/openai";
|
||||
import { ChatPromptTemplate } from "@langchain/core/prompts";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import createBookingIfAvailable from "../tools/createBooking";
|
||||
@@ -47,67 +48,72 @@ const agent = async (
|
||||
/**
|
||||
* Initialize the agent executor with arguments.
|
||||
*/
|
||||
const executor = await initializeAgentExecutorWithOptions(tools, model, {
|
||||
agentArgs: {
|
||||
prefix: `You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
|
||||
Make sure your final answers are definitive, complete and well formatted.
|
||||
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
||||
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
||||
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
|
||||
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
|
||||
const prompt =
|
||||
ChatPromptTemplate.fromTemplate(`You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
|
||||
Make sure your final answers are definitive, complete and well formatted.
|
||||
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
||||
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
||||
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
|
||||
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
|
||||
|
||||
The primary user's id is: ${userId}
|
||||
The primary user's username is: ${user.username}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
The primary user's time zone is: ${user.timeZone}
|
||||
The primary user's event types are: ${user.eventTypes
|
||||
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
|
||||
.join("\n")}
|
||||
The primary user's working hours are: ${user.workingHours
|
||||
.map(
|
||||
(w: WorkingHours) =>
|
||||
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
|
||||
w.startTime
|
||||
}, End Time (minutes in UTC): ${w.endTime};`
|
||||
)
|
||||
.join("\n")}
|
||||
${
|
||||
users.length
|
||||
? `The email references the following @usernames and emails: ${users
|
||||
.map(
|
||||
(u) =>
|
||||
`${
|
||||
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
||||
(u.username
|
||||
? u.type === "fromUsername"
|
||||
? `, username: @${u.username}`
|
||||
: ", username: REDACTED"
|
||||
: ", (no username)") +
|
||||
(u.email
|
||||
? u.type === "fromEmail"
|
||||
? `, email: ${u.email}`
|
||||
: ", email: REDACTED"
|
||||
: ", (no email)")
|
||||
};`
|
||||
)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
}`);
|
||||
const agent = await createOpenAIFunctionsAgent({
|
||||
llm: model,
|
||||
prompt,
|
||||
tools,
|
||||
});
|
||||
|
||||
The primary user's id is: ${userId}
|
||||
The primary user's username is: ${user.username}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
The primary user's time zone is: ${user.timeZone}
|
||||
The primary user's event types are: ${user.eventTypes
|
||||
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
|
||||
.join("\n")}
|
||||
The primary user's working hours are: ${user.workingHours
|
||||
.map(
|
||||
(w: WorkingHours) =>
|
||||
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
|
||||
w.startTime
|
||||
}, End Time (minutes in UTC): ${w.endTime};`
|
||||
)
|
||||
.join("\n")}
|
||||
${
|
||||
users.length
|
||||
? `The email references the following @usernames and emails: ${users
|
||||
.map(
|
||||
(u) =>
|
||||
`${
|
||||
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
||||
(u.username
|
||||
? u.type === "fromUsername"
|
||||
? `, username: @${u.username}`
|
||||
: ", username: REDACTED"
|
||||
: ", (no username)") +
|
||||
(u.email
|
||||
? u.type === "fromEmail"
|
||||
? `, email: ${u.email}`
|
||||
: ", email: REDACTED"
|
||||
: ", (no email)")
|
||||
};`
|
||||
)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
}
|
||||
`,
|
||||
},
|
||||
agentType: "openai-functions",
|
||||
const executor = new AgentExecutor({
|
||||
agent,
|
||||
tools,
|
||||
returnIntermediateSteps: env.NODE_ENV === "development",
|
||||
verbose: env.NODE_ENV === "development",
|
||||
});
|
||||
|
||||
const result = await executor.call({ input });
|
||||
const result = await executor.invoke({ input });
|
||||
const { output } = result;
|
||||
|
||||
return output;
|
||||
|
||||
@@ -74,6 +74,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
theme: true,
|
||||
appTheme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
hideBranding: true,
|
||||
@@ -100,6 +101,7 @@ const schemaUserEditParams = z.object({
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
@@ -122,6 +124,7 @@ const schemaUserCreateParams = z.object({
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
@@ -159,6 +162,7 @@ export const schemaUserReadPublic = User.pick({
|
||||
weekStart: true,
|
||||
endTime: true,
|
||||
bufferTime: true,
|
||||
appTheme: true,
|
||||
theme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
|
||||
@@ -41,6 +41,6 @@
|
||||
"typescript": "^4.9.4",
|
||||
"tzdata": "^1.0.30",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ async function handler(req: NextApiRequest) {
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
userId,
|
||||
returnDateOverrides: true,
|
||||
});
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
@@ -232,6 +233,7 @@ async function handler(req: NextApiRequest) {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
returnDateOverrides: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
@@ -206,7 +207,7 @@ async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) req.userId = req.body.userId || userId;
|
||||
|
||||
return await handleNewBooking(req);
|
||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
||||
@@ -51,7 +51,6 @@ export async function getHandler(req: NextApiRequest) {
|
||||
include: {
|
||||
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 } },
|
||||
|
||||
@@ -56,7 +56,6 @@ async function getHandler(req: NextApiRequest) {
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
users: true,
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
export default function getCalLink(eventType: {
|
||||
team?: { slug: string | null } | null;
|
||||
@@ -6,7 +6,7 @@ export default function getCalLink(eventType: {
|
||||
users?: { username: string | null }[];
|
||||
slug: string;
|
||||
}) {
|
||||
return `${CAL_URL}/${
|
||||
return `${WEBSITE_URL}/${
|
||||
eventType?.team
|
||||
? `team/${eventType?.team?.slug}`
|
||||
: eventType?.owner
|
||||
|
||||
@@ -59,7 +59,6 @@ async function getHandler(req: NextApiRequest) {
|
||||
include: {
|
||||
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 } },
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "highlight.js/styles/default.css";
|
||||
import { type AppProps } from "next/app";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { SwaggerUI } from "swagger-ui-react";
|
||||
|
||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||
|
||||
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
|
||||
const SwaggerUIDynamic = dynamic(() => import("swagger-ui-react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -17,8 +16,6 @@ export default function APIDocs() {
|
||||
requestSnippets={requestSnippets}
|
||||
plugins={[SnippedGenerator]}
|
||||
tryItOutEnabled={true}
|
||||
syntaxHighlight={true}
|
||||
enableCORS={false} // Doesn't seem to work either
|
||||
docExpansion="list"
|
||||
filter={true}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetStaticPropsType } from "next";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
@@ -23,6 +22,6 @@ export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page })<"P">;
|
||||
export default WithLayout({ getLayout: null, getData, Page })<"P">;
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import EventTypes from "~/event-types/views/event-types-listing-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -11,4 +9,4 @@ export const generateMetadata = async () =>
|
||||
(t) => t("event_types_page_subtitle")
|
||||
);
|
||||
|
||||
export default WithLayout({ getLayout, Page: EventTypes })<"P">;
|
||||
export default WithLayout({ getLayout: null, Page: EventTypes })<"P">;
|
||||
|
||||
@@ -3,8 +3,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -16,4 +14,4 @@ export const generateMetadata = async () =>
|
||||
|
||||
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage });
|
||||
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
|
||||
|
||||
@@ -18,7 +18,8 @@ export const generateMetadata = async ({
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
|
||||
const { entity, user, slug, booking } = props;
|
||||
const { eventData, user, slug, booking } = props;
|
||||
const entity = eventData.entity;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
|
||||
|
||||
@@ -16,7 +16,8 @@ export const generateMetadata = async ({
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
|
||||
const { entity, user, slug, booking } = props;
|
||||
const { eventData, user, slug, booking } = props;
|
||||
const entity = eventData.entity;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
|
||||
|
||||
@@ -3,8 +3,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { getServerSideProps } from "@lib/teams/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -13,4 +11,4 @@ export const generateMetadata = async () =>
|
||||
(t) => t("create_manage_teams_collaborative")
|
||||
);
|
||||
|
||||
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout, Page })<"P">;
|
||||
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout: null, Page })<"P">;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import LegacyPage from "@calcom/features/ee/workflows/pages/index";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -10,4 +9,4 @@ export const generateMetadata = async () =>
|
||||
(t) => t("workflows_to_automate_notifications")
|
||||
);
|
||||
|
||||
export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
|
||||
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
|
||||
|
||||
+17
-7
@@ -2,11 +2,9 @@ import { dir } from "i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
@@ -18,6 +16,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "block",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
@@ -69,11 +68,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
|
||||
data-nextjs-router="app">
|
||||
<head nonce={nonce}>
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
<Script
|
||||
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
||||
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
||||
{!!process.env.NEXT_PUBLIC_HEAD_SCRIPTS && (
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_HEAD_SCRIPTS,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<style>{`
|
||||
@@ -97,6 +98,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
}
|
||||
: {}
|
||||
}>
|
||||
{!!process.env.NEXT_PUBLIC_BODY_SCRIPTS && (
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_BODY_SCRIPTS,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,6 +24,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
function PageWrapper(props: AppProps) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useEffect } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
ShellSubHeading,
|
||||
Label,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||
@@ -182,6 +184,15 @@ function ConnectedCalendarsList(props: Props) {
|
||||
export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) {
|
||||
const { t } = useLocale();
|
||||
const { heading = true, fromOnboarding } = props;
|
||||
const { error, setQuery: setError } = useRouterQuery("error");
|
||||
|
||||
useEffect(() => {
|
||||
if (error === "account_already_linked") {
|
||||
showToast(t(error), "error", { id: error });
|
||||
setError(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const utils = trpc.useContext();
|
||||
const onChanged = () =>
|
||||
Promise.allSettled([
|
||||
|
||||
@@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
|
||||
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { AppFrontendPayload } from "@calcom/types/App";
|
||||
@@ -125,7 +125,7 @@ export const InstallAppButtonChild = ({
|
||||
StartIcon={(props) => (
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,11 @@ export default function InstalledAppsLayout({
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
return (
|
||||
<Shell {...rest} hideHeadingOnMobile>
|
||||
<Shell
|
||||
{...rest}
|
||||
title="Installed Apps"
|
||||
description="Manage your installed apps or change settings"
|
||||
hideHeadingOnMobile>
|
||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||
{children}
|
||||
</AppCategoryNavigation>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
@@ -23,13 +24,22 @@ type Props = {
|
||||
theme: string | null;
|
||||
allRemainingBookings: boolean;
|
||||
seatReferenceUid?: string;
|
||||
bookingCancelledEventProps: {
|
||||
booking: unknown;
|
||||
organizer: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone?: string;
|
||||
};
|
||||
eventType: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export default function CancelBooking(props: Props) {
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { booking, allRemainingBookings, seatReferenceUid } = props;
|
||||
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
@@ -97,8 +107,17 @@ export default function CancelBooking(props: Props) {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const bookingWithCancellationReason = {
|
||||
...(bookingCancelledEventProps.booking as object),
|
||||
cancellationReason,
|
||||
} as unknown;
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
// tested by apps/web/playwright/booking-pages.e2e.ts
|
||||
sdkActionManager?.fire("bookingCancelled", {
|
||||
...bookingCancelledEventProps,
|
||||
booking: bookingWithCancellationReason,
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { EventNameObjectType } from "@calcom/core/event";
|
||||
@@ -20,18 +19,21 @@ import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/lib/constants";
|
||||
import { APP_NAME, IS_VISUAL_REGRESSION_TESTING, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Badge,
|
||||
CheckboxField,
|
||||
Label,
|
||||
SelectField,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
@@ -41,22 +43,17 @@ import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||
|
||||
const generateHashedLink = (id: number) => {
|
||||
const translator = short();
|
||||
const seed = `${id}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
return uid;
|
||||
};
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
|
||||
const [useEventTypeDestinationCalendarEmail, setUseEventTypeDestinationCalendarEmail] = useState(
|
||||
formMethods.getValues("useEventTypeDestinationCalendarEmail")
|
||||
);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
@@ -65,7 +62,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = `${name} input`;
|
||||
});
|
||||
@@ -82,7 +78,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
||||
formMethods.getValues("requiresConfirmation")
|
||||
);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled =
|
||||
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
||||
@@ -107,7 +103,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
})
|
||||
}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -125,6 +122,18 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
const verifiedSecondaryEmails = [
|
||||
{
|
||||
label: user?.email || "",
|
||||
value: -1,
|
||||
},
|
||||
...(user?.secondaryEmails || [])
|
||||
.filter((secondaryEmail) => !!secondaryEmail.emailVerified)
|
||||
.map((secondaryEmail) => ({ label: secondaryEmail.email, value: secondaryEmail.id })),
|
||||
];
|
||||
const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -134,7 +143,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
* This will fallback to each user selected destination calendar.
|
||||
*/}
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -180,6 +189,49 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="w-full">
|
||||
<Switch
|
||||
tooltip={t("reconnect_calendar_to_use")}
|
||||
label={
|
||||
<>
|
||||
{t("display_add_to_calendar_organizer")}
|
||||
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
|
||||
</>
|
||||
}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
|
||||
if (val) {
|
||||
showToast(t("reconnect_calendar_to_use"), "warning");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!useEventTypeDestinationCalendarEmail && verifiedSecondaryEmails.length > 0 && (
|
||||
<div className="w-full">
|
||||
<SelectField
|
||||
label={t("send_event_details_to")}
|
||||
placeholder={
|
||||
selectedSecondaryEmailId === -1 && (
|
||||
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
|
||||
<Badge variant="blue">{t("default")}</Badge> {user?.email || ""}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
onChange={(option) =>
|
||||
formMethods.setValue("secondaryEmailId", option?.value, { shouldDirty: true })
|
||||
}
|
||||
value={verifiedSecondaryEmails.find(
|
||||
(secondaryEmail) =>
|
||||
selectedSecondaryEmailId !== -1 && secondaryEmail.value === selectedSecondaryEmailId
|
||||
)}
|
||||
options={verifiedSecondaryEmails}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
@@ -311,7 +363,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -375,10 +427,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
@@ -498,7 +552,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{showEventNameTip && (
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val)}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
|
||||
defaultValue={formMethods.getValues("eventName")}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
|
||||
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
formMethods.setValue("metadata", {
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata",
|
||||
{
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
setValue("availability", value);
|
||||
setValue("availability", value, { shouldDirty: true });
|
||||
},
|
||||
[data]
|
||||
);
|
||||
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!availabilityValue?.value) return;
|
||||
setValue("schedule", availabilityValue.value);
|
||||
setValue("schedule", availabilityValue.value, { shouldDirty: true });
|
||||
}, [availabilityValue, setValue]);
|
||||
|
||||
return (
|
||||
@@ -260,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
if (selected?.value) setValue("availability", selected);
|
||||
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
|
||||
}}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
value={availabilityValue}
|
||||
@@ -298,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (!checked) {
|
||||
setValue("schedule", null);
|
||||
setValue("schedule", null, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
|
||||
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"bookingLimits",
|
||||
{
|
||||
PER_DAY: 1,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
onChange({
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
onChange({});
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="periodType"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value && value !== "UNLIMITED";
|
||||
|
||||
return (
|
||||
@@ -353,11 +360,18 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
onCheckedChange={(bool) => {
|
||||
if (bool && !formMethods.getValues("periodDays")) {
|
||||
formMethods.setValue("periodDays", 30, { shouldDirty: true });
|
||||
}
|
||||
return onChange(bool ? "ROLLING" : "UNLIMITED");
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<RadioGroup.Root
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
@@ -388,17 +402,21 @@ export const EventLimitsTab = () => {
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
|
||||
})}
|
||||
defaultValue={optionsPeriod.find(
|
||||
(opt) =>
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -407,12 +425,12 @@ export const EventLimitsTab = () => {
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
render={() => (
|
||||
render={({ field: { onChange } }) => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
onChange({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
@@ -446,7 +464,7 @@ export const EventLimitsTab = () => {
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
if (!active) {
|
||||
formMethods.setValue("offsetStart", 0);
|
||||
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -577,11 +595,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
|
||||
setValue(
|
||||
propertyName,
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
{
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -611,7 +634,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||
return mounted ? (
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={isEditable}
|
||||
@@ -318,7 +318,7 @@ export const EventSetupTab = (
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage
|
||||
errors={formMethods.formState.errors.locations?.[index]}
|
||||
errors={formMethods.formState.errors?.locations?.[index]}
|
||||
name={eventLocationType.defaultValueVariable}
|
||||
className="text-error my-1 ml-6 text-sm"
|
||||
as="div"
|
||||
@@ -401,7 +401,7 @@ export const EventSetupTab = (
|
||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||
Change it{" "}
|
||||
<Link
|
||||
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||
href={`${WEBAPP_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||
className="underline">
|
||||
here.
|
||||
</Link>{" "}
|
||||
@@ -412,7 +412,7 @@ export const EventSetupTab = (
|
||||
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
|
||||
<p className="pl-1 text-sm leading-none text-red-600">
|
||||
{t("app_not_connected", { appName: locationDetails.name })}{" "}
|
||||
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
|
||||
<a className="underline" href={`${WEBAPP_URL}/apps/${locationDetails.slug}`}>
|
||||
{t("connect_now")}
|
||||
</a>
|
||||
</p>
|
||||
@@ -444,9 +444,10 @@ export const EventSetupTab = (
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
|
||||
const urlPrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -511,16 +512,16 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -541,7 +542,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -570,12 +571,12 @@ export const EventSetupTab = (
|
||||
setMultipleDuration(undefined);
|
||||
setSelectedMultipleDuration([]);
|
||||
setDefaultDuration(null);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("length", eventType.length, { shouldDirty: true });
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
|
||||
formMethods.setValue("length", 0, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps, FormValues, Host } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ComponentProps, Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
@@ -21,6 +21,13 @@ interface IUserToValue {
|
||||
email: string;
|
||||
}
|
||||
|
||||
type TeamMember = {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
|
||||
value: `${id || ""}`,
|
||||
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
|
||||
@@ -109,7 +116,7 @@ const AssignAllTeamMembers = ({
|
||||
onInactive?: () => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
@@ -117,10 +124,10 @@ const AssignAllTeamMembers = ({
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("automatically_add_all_team_members")}
|
||||
description={t("including_people_who_join_in_future")}
|
||||
labelClassName="mt-0.5 font-normal"
|
||||
checked={assignAllTeamMembers}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("assignAllTeamMembers", active);
|
||||
setValue("assignAllTeamMembers", active, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(active);
|
||||
if (active) {
|
||||
onActive();
|
||||
@@ -128,7 +135,6 @@ const AssignAllTeamMembers = ({
|
||||
onInactive();
|
||||
}
|
||||
}}
|
||||
toggleSwitchAtTheEnd
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -148,13 +154,13 @@ const CheckedHostField = ({
|
||||
labelText?: string;
|
||||
placeholder: string;
|
||||
isFixed: boolean;
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
|
||||
value: Host[];
|
||||
onChange?: (options: Host[]) => void;
|
||||
options?: Options<CheckedSelectOption>;
|
||||
helperText?: React.ReactNode | string;
|
||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||
return (
|
||||
<div className="bg-muted flex flex-col space-y-5 p-4">
|
||||
<div className="flex flex-col rounded-md">
|
||||
<div>
|
||||
{labelText ? <Label>{labelText}</Label> : <></>}
|
||||
<CheckedTeamSelect
|
||||
@@ -165,23 +171,22 @@ const CheckedHostField = ({
|
||||
options.map((option) => ({
|
||||
isFixed,
|
||||
userId: parseInt(option.value, 10),
|
||||
priority: option.priority ?? 2,
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={(value || [])
|
||||
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.map((host) => {
|
||||
const option = options.find((member) => member.value === host.userId.toString());
|
||||
return option ? { ...option, priority: host.priority ?? 2, isFixed } : options[0];
|
||||
})
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
{helperText && <p className="text-subtle text-sm">{helperText}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -199,6 +204,158 @@ const FixedHostHelper = (
|
||||
</Trans>
|
||||
);
|
||||
|
||||
const FixedHosts = ({
|
||||
teamMembers,
|
||||
value,
|
||||
onChange,
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
isRoundRobinEvent = false,
|
||||
}: {
|
||||
value: Host[];
|
||||
onChange: (hosts: Host[]) => void;
|
||||
teamMembers: TeamMember[];
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
isRoundRobinEvent?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { getValues, setValue } = useFormContext<FormValues>();
|
||||
|
||||
const hasActiveFixedHosts = isRoundRobinEvent && getValues("hosts").some((host) => host.isFixed);
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(hasActiveFixedHosts);
|
||||
|
||||
return (
|
||||
<div className="mt-5 rounded-lg">
|
||||
{!isRoundRobinEvent ? (
|
||||
<>
|
||||
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
|
||||
<Label className="mb-1 text-sm font-semibold">{t("fixed_hosts")}</Label>
|
||||
<p className="text-subtle max-w-full break-words text-sm leading-tight">{FixedHostHelper}</p>
|
||||
</div>
|
||||
<div className="border-subtle rounded-b-md border border-t-0">
|
||||
<AddMembersWithSwitch
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
automaticAddAllEnabled={!isRoundRobinEvent}
|
||||
isFixed={true}
|
||||
onActive={() =>
|
||||
setValue(
|
||||
"hosts",
|
||||
teamMembers.map((teamMember) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("fixed_hosts")}
|
||||
description={FixedHostHelper}
|
||||
checked={isDisabled}
|
||||
labelClassName="text-sm"
|
||||
descriptionClassName=" text-sm text-subtle"
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked) {
|
||||
const rrHosts = getValues("hosts")
|
||||
.filter((host) => !host.isFixed)
|
||||
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
|
||||
setValue("hosts", rrHosts, { shouldDirty: true });
|
||||
}
|
||||
setIsDisabled(checked);
|
||||
}}
|
||||
childrenClassName="lg:ml-0">
|
||||
<div className="border-subtle flex flex-col gap-6 rounded-bl-md rounded-br-md border border-t-0">
|
||||
<AddMembersWithSwitch
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
automaticAddAllEnabled={!isRoundRobinEvent}
|
||||
isFixed={true}
|
||||
onActive={() =>
|
||||
setValue(
|
||||
"hosts",
|
||||
teamMembers.map((teamMember) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AddMembersWithSwitch = ({
|
||||
teamMembers,
|
||||
value,
|
||||
onChange,
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
automaticAddAllEnabled,
|
||||
onActive,
|
||||
isFixed,
|
||||
}: {
|
||||
value: Host[];
|
||||
onChange: (hosts: Host[]) => void;
|
||||
teamMembers: TeamMember[];
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
automaticAddAllEnabled: boolean;
|
||||
onActive: () => void;
|
||||
isFixed: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<div className="rounded-md ">
|
||||
<div className="flex flex-col rounded-md px-6 pb-2 pt-6">
|
||||
{automaticAddAllEnabled ? (
|
||||
<div className="mb-2">
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={onActive}
|
||||
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{!assignAllTeamMembers || !automaticAddAllEnabled ? (
|
||||
<CheckedHostField
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isFixed={isFixed}
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RoundRobinHosts = ({
|
||||
teamMembers,
|
||||
value,
|
||||
@@ -206,68 +363,45 @@ const RoundRobinHosts = ({
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
}: {
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
value: Host[];
|
||||
onChange: (hosts: Host[]) => void;
|
||||
teamMembers: TeamMember[];
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<div className="border-subtle mt-6 space-y-5 rounded-lg border px-4 py-6 sm:px-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
isFixed={true}
|
||||
onChange={(changeValue) => {
|
||||
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
|
||||
}}
|
||||
<div className="rounded-lg ">
|
||||
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
|
||||
<Label className="mb-1 text-sm font-semibold">{t("round_robin_hosts")}</Label>
|
||||
<p className="text-subtle max-w-full break-words text-sm leading-tight">{t("round_robin_helper")}</p>
|
||||
</div>
|
||||
<div className="border-subtle rounded-b-md border border-t-0">
|
||||
<AddMembersWithSwitch
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
helperText={FixedHostHelper}
|
||||
onChange={onChange}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
automaticAddAllEnabled={true}
|
||||
isFixed={false}
|
||||
onActive={() =>
|
||||
setValue(
|
||||
"hosts",
|
||||
teamMembers
|
||||
.map((teamMember) => ({
|
||||
isFixed: false,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
.sort((a, b) => b.priority - a.priority),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="bg-muted flex flex-col">
|
||||
<Label className="p-4">{t("round_robin_hosts")}</Label>
|
||||
<div className="px-4 pb-2">
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() =>
|
||||
formMethods.setValue(
|
||||
"hosts",
|
||||
teamMembers.map((teamMember) => ({
|
||||
isFixed: false,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
}))
|
||||
)
|
||||
}
|
||||
onInactive={() => formMethods.setValue("hosts", [])}
|
||||
/>
|
||||
</div>
|
||||
{assignAllTeamMembers ? (
|
||||
<div className="mb-4" />
|
||||
) : (
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
onChange={(changeValue) =>
|
||||
onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])
|
||||
}
|
||||
value={value}
|
||||
isFixed={false}
|
||||
placeholder={t("add_attendees")}
|
||||
helperText={t("round_robin_helper")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -282,14 +416,14 @@ const ChildrenEventTypes = ({
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
return (
|
||||
<div className="border-subtle mt-6 space-y-5 rounded-lg border px-4 py-6 sm:px-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() => formMethods.setValue("children", childrenEventTypeOptions)}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
|
||||
/>
|
||||
{!assignAllTeamMembers ? (
|
||||
<Controller<FormValues>
|
||||
@@ -311,19 +445,14 @@ const Hosts = ({
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
}: {
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
teamMembers: TeamMember[];
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
@@ -343,66 +472,52 @@ const Hosts = ({
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
setValue(
|
||||
"hosts",
|
||||
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [schedulingType, setValue, getValues, submitCount]);
|
||||
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
name="hosts"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const schedulingTypeRender = {
|
||||
COLLECTIVE: (
|
||||
<div className="border-subtle mt-6 space-y-5 rounded-lg border px-4 py-6 sm:px-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() =>
|
||||
formMethods.setValue(
|
||||
"hosts",
|
||||
teamMembers.map((teamMember) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
}))
|
||||
)
|
||||
}
|
||||
onInactive={() => formMethods.setValue("hosts", [])}
|
||||
/>
|
||||
{assignAllTeamMembers ? (
|
||||
<></>
|
||||
) : (
|
||||
<CheckedHostField
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isFixed={true}
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FixedHosts
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
/>
|
||||
),
|
||||
ROUND_ROBIN: (
|
||||
<>
|
||||
<RoundRobinHosts
|
||||
<FixedHosts
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={(changeValue) => {
|
||||
onChange([...value.filter((host: Host) => !host.isFixed), ...changeValue]);
|
||||
}}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
isRoundRobinEvent={true}
|
||||
/>
|
||||
<RoundRobinHosts
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={(changeValue) => {
|
||||
onChange(
|
||||
[...value.filter((host: Host) => host.isFixed), ...changeValue].sort(
|
||||
(a, b) => b.priority - a.priority
|
||||
)
|
||||
);
|
||||
}}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
teamMembers={teamMembers}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</>
|
||||
),
|
||||
MANAGED: <></>,
|
||||
@@ -449,39 +564,47 @@ export const EventTeamTab = ({
|
||||
);
|
||||
});
|
||||
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { getValues, setValue } = useFormContext<FormValues>();
|
||||
const [assignAllTeamMembers, setAssignAllTeamMembers] = useState<boolean>(
|
||||
formMethods.getValues("assignAllTeamMembers") ?? false
|
||||
getValues("assignAllTeamMembers") ?? false
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && !isManagedEventType && (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
<Controller<FormValues>
|
||||
name="schedulingType"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
options={schedulingTypeOptions}
|
||||
value={schedulingTypeOptions.find((opt) => opt.value === value)}
|
||||
className="w-full"
|
||||
onChange={(val) => {
|
||||
onChange(val?.value);
|
||||
formMethods.setValue("assignAllTeamMembers", false);
|
||||
setAssignAllTeamMembers(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<div className="border-subtle flex flex-col rounded-md">
|
||||
<div className="border-subtle rounded-t-md border p-6 pb-5">
|
||||
<Label className="mb-1 text-sm font-semibold">{t("assignment")}</Label>
|
||||
<p className="text-subtle max-w-full break-words text-sm leading-tight">
|
||||
{t("assignment_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-subtle rounded-b-md border border-t-0 p-6">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
<Controller<FormValues>
|
||||
name="schedulingType"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
options={schedulingTypeOptions}
|
||||
value={schedulingTypeOptions.find((opt) => opt.value === value)}
|
||||
className="w-full"
|
||||
onChange={(val) => {
|
||||
onChange(val?.value);
|
||||
setValue("assignAllTeamMembers", false, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Hosts
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
teamMembers={teamMembersOptions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{team && isManagedEventType && (
|
||||
<ChildrenEventTypes
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -133,6 +134,60 @@ function getNavigation({
|
||||
];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
children,
|
||||
eventType,
|
||||
@@ -149,9 +204,8 @@ function EventTypeSingleLayout({
|
||||
bookerUrl,
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
@@ -160,24 +214,6 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
@@ -308,7 +344,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -352,7 +388,7 @@ function EventTypeSingleLayout({
|
||||
StartIcon={Code}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
namespace={eventType.slug}
|
||||
namespace=""
|
||||
tooltip={t("embed")}
|
||||
tooltipSide="bottom"
|
||||
tooltipOffset={4}
|
||||
@@ -424,7 +460,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -435,6 +471,7 @@ function EventTypeSingleLayout({
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={isUpdateMutationLoading}
|
||||
disabled={!formMethods.formState.isDirty}
|
||||
data-testid="update-eventtype"
|
||||
form="event-type-form">
|
||||
{t("save")}
|
||||
@@ -467,31 +504,13 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: formMethods.getValues("id") });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
eventTypeId={eventType.id}
|
||||
isManagedEvent={isManagedEvent}
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
/>
|
||||
|
||||
<EventTypeEmbedDialog />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,10 @@ export default function InstantEventController({
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("isInstantEvent", false);
|
||||
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
|
||||
setInstantEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("isInstantEvent", true);
|
||||
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
|
||||
setInstantEventState(true);
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function RecurringEventController({
|
||||
data-testid="recurring-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = eventType.recurringEvent || {
|
||||
@@ -74,7 +74,7 @@ export default function RecurringEventController({
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
@@ -95,7 +95,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
|
||||
|
||||
useEffect(() => {
|
||||
if (!requiresConfirmation) {
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
@@ -82,7 +82,7 @@ export default function RequiresConfirmationController({
|
||||
checked={requiresConfirmation}
|
||||
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||
onCheckedChange={(val) => {
|
||||
formMethods.setValue("requiresConfirmation", val);
|
||||
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
|
||||
onRequiresConfirmation(val);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -96,16 +96,19 @@ export default function RequiresConfirmationController({
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "always") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setRequiresConfirmationSetup(undefined);
|
||||
} else if (val === "notice") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold",
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
}}>
|
||||
@@ -146,7 +149,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.time",
|
||||
val
|
||||
val,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
|
||||
@@ -171,7 +175,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
opt?.value as UnitTypeLongPlural,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -37,7 +37,7 @@ const UserProfile = () => {
|
||||
if (context.avatar) {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.refetch();
|
||||
} else {
|
||||
} else
|
||||
try {
|
||||
if (eventTypes?.length === 0) {
|
||||
await Promise.all(
|
||||
@@ -50,9 +50,11 @@ const UserProfile = () => {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
await utils.viewer.me.refetch();
|
||||
router.push("/");
|
||||
}
|
||||
await utils.viewer.me.refetch();
|
||||
const redirectUrl = localStorage.getItem("onBoardingRedirect");
|
||||
localStorage.removeItem("onBoardingRedirect");
|
||||
|
||||
redirectUrl ? router.push(redirectUrl) : router.push("/");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("problem_saving_user_profile"), "error");
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { FormValues } from "@pages/settings/my-account/profile";
|
||||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Badge,
|
||||
TextField,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Button,
|
||||
InputError,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Flag, Trash, Send } from "@calcom/ui/components/icon";
|
||||
|
||||
type CustomEmailTextFieldProps = {
|
||||
formMethods: UseFormReturn<FormValues>;
|
||||
formMethodFieldName: keyof FormValues;
|
||||
errorMessage: string;
|
||||
emailVerified: boolean;
|
||||
emailPrimary: boolean;
|
||||
dataTestId: string;
|
||||
handleChangePrimary: () => void;
|
||||
handleVerifyEmail: () => void;
|
||||
handleItemDelete: () => void;
|
||||
};
|
||||
|
||||
const CustomEmailTextField = ({
|
||||
formMethods,
|
||||
formMethodFieldName,
|
||||
errorMessage,
|
||||
emailVerified,
|
||||
emailPrimary,
|
||||
dataTestId,
|
||||
handleChangePrimary,
|
||||
handleVerifyEmail,
|
||||
handleItemDelete,
|
||||
}: CustomEmailTextFieldProps) => {
|
||||
const { t } = useLocale();
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`border-default mt-2 flex items-center rounded-md border ${
|
||||
inputFocus ? "ring-brand-default border-neutral-300 ring-2" : ""
|
||||
}`}>
|
||||
<TextField
|
||||
{...formMethods.register(formMethodFieldName)}
|
||||
label=""
|
||||
containerClassName="flex flex-1 items-center"
|
||||
className="mb-0 border-none outline-none focus:ring-0"
|
||||
data-testid={dataTestId}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
/>
|
||||
<div className="flex items-center pr-2">
|
||||
{emailPrimary && (
|
||||
<Badge variant="blue" size="sm" data-testid={`${dataTestId}-primary-badge`}>
|
||||
{t("primary")}
|
||||
</Badge>
|
||||
)}
|
||||
{!emailVerified && (
|
||||
<Badge variant="orange" size="sm" className="ml-2" data-testid={`${dataTestId}-unverified-badge`}>
|
||||
{t("unverified")}
|
||||
</Badge>
|
||||
)}
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
StartIcon={MoreHorizontal}
|
||||
variant="icon"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className="ml-2 rounded-md"
|
||||
data-testid="secondary-email-action-group-button"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Flag}
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleChangePrimary}
|
||||
disabled={!emailVerified || emailPrimary}
|
||||
data-testid="secondary-email-make-primary-button">
|
||||
{t("make_primary")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
{!emailVerified && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Send}
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleVerifyEmail}
|
||||
disabled={emailVerified}
|
||||
data-testid="resend-verify-email-button">
|
||||
{t("resend_email")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Trash}
|
||||
color="destructive"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleItemDelete}
|
||||
disabled={emailPrimary}
|
||||
data-testid="secondary-email-delete-button">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{errorMessage && <InputError message={errorMessage} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEmailTextField;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui";
|
||||
|
||||
interface SecondaryEmailConfirmModalProps {
|
||||
email: string;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const SecondaryEmailConfirmModal = ({ email, onCancel }: SecondaryEmailConfirmModalProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent
|
||||
title={t("confirm_email")}
|
||||
description={<Trans i18nKey="confirm_email_description" values={{ email }} />}
|
||||
type="creation"
|
||||
data-testid="secondary-email-confirm-dialog">
|
||||
<DialogFooter>
|
||||
<DialogClose color="primary" onClick={onCancel} data-testid="secondary-email-confirm-done-button">
|
||||
{t("done")}
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondaryEmailConfirmModal;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
TextField,
|
||||
Form,
|
||||
InputError,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface SecondaryEmailModalProps {
|
||||
isLoading: boolean;
|
||||
errorMessage?: string;
|
||||
handleAddEmail: (value: { email: string }) => void;
|
||||
onCancel: () => void;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const SecondaryEmailModal = ({
|
||||
isLoading,
|
||||
errorMessage,
|
||||
handleAddEmail,
|
||||
onCancel,
|
||||
clearErrorMessage,
|
||||
}: SecondaryEmailModalProps) => {
|
||||
const { t } = useLocale();
|
||||
type FormValues = {
|
||||
email: string;
|
||||
};
|
||||
const formMethods = useForm<FormValues>({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// We will reset the errorMessage once the user starts modifying the email
|
||||
const subscription = formMethods.watch(() => clearErrorMessage());
|
||||
return () => subscription.unsubscribe();
|
||||
}, [formMethods.watch]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent
|
||||
title={t("add_email")}
|
||||
description={t("add_email_description")}
|
||||
type="creation"
|
||||
data-testid="secondary-email-add-dialog">
|
||||
<Form form={formMethods} handleSubmit={handleAddEmail}>
|
||||
<TextField
|
||||
label={t("email_address")}
|
||||
data-testid="secondary-email-input"
|
||||
{...formMethods.register("email")}
|
||||
/>
|
||||
{errorMessage && <InputError message={errorMessage} />}
|
||||
<DialogFooter showDivider className="mt-10">
|
||||
<DialogClose onClick={onCancel}>{t("cancel")}</DialogClose>
|
||||
<Button type="submit" data-testid="add-secondary-email-button" disabled={isLoading}>
|
||||
{t("add_email")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondaryEmailModal;
|
||||
@@ -160,7 +160,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
<div className="bg-subtle flex w-full flex-wrap gap-6 rounded-sm px-2 py-3 text-sm">
|
||||
<div>
|
||||
<p className="text-subtle">{t("current_username")}</p>
|
||||
<p className="text-emphasis mt-1" data-testid="current-username">
|
||||
<p className="text-emphasis mt-1 break-all" data-testid="current-username">
|
||||
{currentUsername}
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,7 +168,10 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
<p className="text-subtle" data-testid="new-username">
|
||||
{t("new_username")}
|
||||
</p>
|
||||
<p className="text-emphasis mt-1">{inputUsernameValue}</p>
|
||||
|
||||
<p className="text-emphasis min-h-5 mt-1 max-h-12 overflow-y-auto break-all">
|
||||
{inputUsernameValue}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
@@ -45,7 +45,7 @@ export const UsernameAvailabilityField = ({
|
||||
|
||||
const usernamePrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
||||
<Select<LocationOption>
|
||||
name="location"
|
||||
id="location-select"
|
||||
data-testid="location-select"
|
||||
components={{
|
||||
Option: (props) => {
|
||||
return (
|
||||
|
||||
@@ -113,7 +113,7 @@ const enum ThemeSupport {
|
||||
// e.g. Login Page
|
||||
None = "none",
|
||||
// Entire App except Booking Pages
|
||||
App = "systemOnly",
|
||||
App = "userConfigured",
|
||||
// Booking Pages(including Routing Forms)
|
||||
Booking = "userConfigured",
|
||||
}
|
||||
|
||||
@@ -127,9 +127,9 @@ const enum ThemeSupport {
|
||||
// e.g. Login Page
|
||||
None = "none",
|
||||
// Entire App except Booking Pages
|
||||
App = "systemOnly",
|
||||
App = "appConfigured",
|
||||
// Booking Pages(including Routing Forms)
|
||||
Booking = "userConfigured",
|
||||
Booking = "bookingConfigured",
|
||||
}
|
||||
|
||||
type CalcomThemeProps = PropsWithChildren<
|
||||
@@ -144,8 +144,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
const embedNamespace = getEmbedNamespace(props.router.query);
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
|
||||
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
|
||||
return (
|
||||
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace })}>
|
||||
<ThemeProvider {...themeProviderProps}>
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
|
||||
@@ -11,10 +11,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@ export default function useRouterQuery<T extends string>(name: T) {
|
||||
const setQuery = useCallback(
|
||||
(newValue: string | number | null | undefined) => {
|
||||
const _searchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
_searchParams.set(name, newValue as string);
|
||||
if (typeof newValue === "undefined") {
|
||||
// when newValue is of type undefined, clear the search param.
|
||||
_searchParams.delete(name);
|
||||
} else {
|
||||
_searchParams.set(name, newValue as string);
|
||||
}
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
},
|
||||
[name, pathname, router, searchParams]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
// If feature flag is disabled, return not found on getServerSideProps
|
||||
export const getServerSideProps = async () => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const insightsEnabled = await getFeatureFlag(prisma, "insights");
|
||||
|
||||
if (flags.insights === false) {
|
||||
if (!insightsEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
|
||||
@@ -696,7 +696,7 @@ describe("orgMigration", () => {
|
||||
});
|
||||
|
||||
describe("when user email matches orgAutoAcceptEmail", () => {
|
||||
const orgMetadata = {
|
||||
const orgSettings = {
|
||||
orgAutoAcceptEmail: "org1.com",
|
||||
} as const;
|
||||
|
||||
@@ -725,8 +725,10 @@ describe("orgMigration", () => {
|
||||
const dbOrg = await createOrg({
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
...orgMetadata,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -784,8 +786,10 @@ describe("orgMigration", () => {
|
||||
const dbOrg = await createOrg({
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
...orgMetadata,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -837,7 +841,11 @@ describe("orgMigration", () => {
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
requestedSlug: data.targetOrg.requestedSlug,
|
||||
...orgMetadata,
|
||||
},
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -901,9 +909,7 @@ describe("orgMigration", () => {
|
||||
id: data.targetOrg.id,
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
},
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1564,9 +1570,9 @@ async function createOrg(
|
||||
return await prismock.team.create({
|
||||
data: {
|
||||
...data,
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
...(data.metadata || {}),
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function moveUserToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
if (!teamMetadata?.isOrganization) {
|
||||
if (!team.isOrganization) {
|
||||
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function moveUserToOrg({
|
||||
if (!targetOrgUsername) {
|
||||
targetOrgUsername = getOrgUsernameFromEmail(
|
||||
userToMoveToOrg.email,
|
||||
targetOrganization.metadata.orgAutoAcceptEmail || ""
|
||||
targetOrganization.organizationSettings?.orgAutoAcceptEmail || ""
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export async function moveTeamToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
|
||||
|
||||
if (!teamMetadata?.isOrganization) {
|
||||
if (!possibleOrg.isOrganization) {
|
||||
throw new Error(`${targetOrg.id} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export async function moveTeamToOrg({
|
||||
await addTeamRedirect({
|
||||
oldTeamSlug,
|
||||
teamSlug: updatedTeam.slug,
|
||||
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
|
||||
orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null,
|
||||
});
|
||||
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
|
||||
if (moveMembers) {
|
||||
@@ -329,13 +329,14 @@ async function setOrgSlugIfNotSet(
|
||||
},
|
||||
orgMetadata: {
|
||||
requestedSlug?: string | null | undefined;
|
||||
},
|
||||
} | null,
|
||||
targetOrgId: number
|
||||
) {
|
||||
if (targetOrganization.slug) {
|
||||
return;
|
||||
}
|
||||
if (!orgMetadata.requestedSlug) {
|
||||
|
||||
if (!orgMetadata?.requestedSlug) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
|
||||
@@ -372,6 +373,9 @@ async function getTeamOrThrowError(targetOrgId: number) {
|
||||
where: {
|
||||
id: targetOrgId,
|
||||
},
|
||||
include: {
|
||||
organizationSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
@@ -650,6 +654,7 @@ async function moveTeamsWithoutMembersToOrg({
|
||||
id: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -661,7 +666,7 @@ async function moveTeamsWithoutMembersToOrg({
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !team.metadata?.isOrganization);
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
|
||||
|
||||
@@ -814,6 +819,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
|
||||
id: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -825,7 +831,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !team.metadata?.isOrganization);
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const organizations = await getFeatureFlag(prisma, "organizations");
|
||||
// Check if organizations are enabled
|
||||
if (flags["organizations"] !== true) {
|
||||
if (!organizations) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -24,11 +24,22 @@ const querySchema = z.object({
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
|
||||
const ssr = await ssrInit(ctx);
|
||||
const token = z.string().optional().parse(ctx.query.token);
|
||||
const redirectUrlData = z
|
||||
.string()
|
||||
.refine((value) => value.startsWith(WEBAPP_URL), {
|
||||
params: (value: string) => ({ value }),
|
||||
message: "Redirect URL must start with 'cal.com'",
|
||||
})
|
||||
.optional()
|
||||
.safeParse(ctx.query.redirect);
|
||||
|
||||
const redirectUrl = redirectUrlData.success && redirectUrlData.data ? redirectUrlData.data : null;
|
||||
|
||||
const props = {
|
||||
redirectUrl,
|
||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||
isSAMLLoginEnabled,
|
||||
trpcState: ssr.dehydrate(),
|
||||
@@ -38,7 +49,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
// username + email prepopulated from query params
|
||||
const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
|
||||
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
@@ -67,14 +78,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
team: {
|
||||
select: {
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,8 +137,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
};
|
||||
|
||||
const isATeamInOrganization = tokenTeam?.parentId !== null;
|
||||
const isOrganization = tokenTeam.metadata?.isOrganization;
|
||||
// Detect if the team is an org by either the metadata flag or if it has a parent team
|
||||
const isOrganization = tokenTeam.isOrganization;
|
||||
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
|
||||
// If we are dealing with an org, the slug may come from the team itself or its parent
|
||||
const orgSlug = isOrganizationOrATeamInOrganization
|
||||
@@ -141,9 +155,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
const isValidEmail = checkValidEmail(verificationToken.identifier);
|
||||
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
|
||||
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
|
||||
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
|
||||
: null;
|
||||
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -156,15 +168,15 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
? getOrgUsernameFromEmail(
|
||||
verificationToken.identifier,
|
||||
(isOrganization
|
||||
? tokenTeam.metadata?.orgAutoAcceptEmail
|
||||
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
|
||||
? tokenTeam.organizationSettings?.orgAutoAcceptEmail
|
||||
: parentOrgSettings?.orgAutoAcceptEmail) || ""
|
||||
)
|
||||
: slugify(username),
|
||||
}
|
||||
: null,
|
||||
orgSlug,
|
||||
orgAutoAcceptEmail: isOrgInviteByLink
|
||||
? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null
|
||||
? tokenTeam?.organizationSettings?.orgAutoAcceptEmail ?? parentOrgSettings?.orgAutoAcceptEmail ?? null
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
@@ -82,12 +82,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
return {
|
||||
props: {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
booking,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
@@ -97,6 +96,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: eventData?.team?.parent?.bannerUrl ?? "",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
@@ -37,12 +37,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
// Provided by Rewrite from next.config.js
|
||||
const isOrgProfile = context.query?.isOrgProfile === "1";
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const isOrganizationFeatureEnabled = flags["organizations"];
|
||||
const organizationsEnabled = await getFeatureFlag(prisma, "organizations");
|
||||
|
||||
log.debug("getServerSideProps", {
|
||||
isOrgProfile,
|
||||
isOrganizationFeatureEnabled,
|
||||
isOrganizationFeatureEnabled: organizationsEnabled,
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
});
|
||||
@@ -73,8 +72,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
// Taking care of sub-teams and orgs
|
||||
if (
|
||||
(!isValidOrgDomain && team?.parent) ||
|
||||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
|
||||
!isOrganizationFeatureEnabled
|
||||
(!isValidOrgDomain && !!team?.isOrganization) ||
|
||||
!organizationsEnabled
|
||||
) {
|
||||
return { notFound: true } as const;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle"
|
||||
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
|
||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
@@ -147,7 +147,13 @@ export default function Bookings() {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
hideHeadingOnMobile
|
||||
heading={t("bookings")}
|
||||
subtitle={t("bookings_description")}
|
||||
title="Bookings"
|
||||
description="Create events to share for people to book on your calendar.">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
@@ -247,6 +253,6 @@ export default function Bookings() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</ShellMain>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
userPrimaryEmail: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getEventName } from "@calcom/core/event";
|
||||
import type { ConfigType } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
@@ -193,22 +192,6 @@ export default function Success(props: PageProps) {
|
||||
}, [telemetry]); */
|
||||
|
||||
useEffect(() => {
|
||||
const users = eventType.users;
|
||||
if (!sdkActionManager) return;
|
||||
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
|
||||
sdkActionManager.fire("bookingSuccessful", {
|
||||
booking: bookingInfo,
|
||||
eventType,
|
||||
date: date.toString(),
|
||||
duration: calculatedDuration,
|
||||
organizer: {
|
||||
name: users[0].name || "Nameless",
|
||||
email: users[0].email || "Email-less",
|
||||
timeZone: users[0].timeZone,
|
||||
},
|
||||
confirmed: !needsConfirmation,
|
||||
// TODO: Add payment details
|
||||
});
|
||||
setDate(
|
||||
date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess() || "Europe/London")
|
||||
);
|
||||
@@ -294,6 +277,16 @@ export default function Success(props: PageProps) {
|
||||
const providerName = guessEventLocationType(location)?.label;
|
||||
const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label;
|
||||
|
||||
const bookingCancelledEventProps = {
|
||||
booking: bookingInfo,
|
||||
organizer: {
|
||||
name: bookingInfo?.user?.name || "Nameless",
|
||||
email: bookingInfo?.userPrimaryEmail || bookingInfo?.user?.email || "Email-less",
|
||||
timeZone: bookingInfo?.user?.timeZone,
|
||||
},
|
||||
eventType,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
||||
{!isEmbed && (
|
||||
@@ -450,7 +443,9 @@ export default function Success(props: PageProps) {
|
||||
</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
<p className="text-default">{bookingInfo.user.email}</p>
|
||||
<p className="text-default">
|
||||
{bookingInfo?.userPrimaryEmail ?? bookingInfo.user.email}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
@@ -608,6 +603,7 @@ export default function Success(props: PageProps) {
|
||||
theme={isSuccessBookingPage ? props.profile.theme : "light"}
|
||||
allRemainingBookings={allRemainingBookings}
|
||||
seatReferenceUid={seatReferenceUid}
|
||||
bookingCancelledEventProps={bookingCancelledEventProps}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -16,18 +16,20 @@ import CreateEventTypeDialog from "@calcom/features/eventtypes/components/Create
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
@@ -76,6 +78,7 @@ import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||
|
||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
|
||||
|
||||
@@ -89,16 +92,27 @@ interface EventTypeListHeadingProps {
|
||||
type EventTypeGroup = EventTypeGroups[number];
|
||||
type EventType = EventTypeGroup["eventTypes"][number];
|
||||
|
||||
type DeNormalizedEventType = Omit<EventType, "userIds"> & {
|
||||
users: (Pick<User, "id" | "name" | "username" | "avatarUrl"> & {
|
||||
nonProfileUsername: string | null;
|
||||
profile: UserProfile;
|
||||
})[];
|
||||
};
|
||||
|
||||
type DeNormalizedEventTypeGroup = Omit<EventTypeGroup, "eventTypes"> & {
|
||||
eventTypes: DeNormalizedEventType[];
|
||||
};
|
||||
|
||||
interface EventTypeListProps {
|
||||
group: EventTypeGroup;
|
||||
group: DeNormalizedEventTypeGroup;
|
||||
groupIndex: number;
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
types: EventType[];
|
||||
types: DeNormalizedEventType[];
|
||||
}
|
||||
|
||||
interface MobileTeamsTabProps {
|
||||
eventTypeGroups: EventTypeGroups;
|
||||
eventTypeGroups: DeNormalizedEventTypeGroup[];
|
||||
}
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -136,7 +150,15 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
|
||||
const Item = ({
|
||||
type,
|
||||
group,
|
||||
readOnly,
|
||||
}: {
|
||||
type: DeNormalizedEventType;
|
||||
group: DeNormalizedEventTypeGroup;
|
||||
readOnly: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const content = () => (
|
||||
@@ -166,11 +188,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
return readOnly ? (
|
||||
<div className="flex-1 overflow-hidden pr-4 text-sm">
|
||||
{content()}
|
||||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={type}
|
||||
shortenDescription
|
||||
/>
|
||||
<EventTypeDescription eventType={type} shortenDescription />
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
@@ -197,7 +215,6 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
)}
|
||||
</div>
|
||||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
|
||||
shortenDescription
|
||||
/>
|
||||
@@ -243,7 +260,7 @@ export const EventTypeList = ({
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
|
||||
if (previousValue) {
|
||||
const newList = [...types];
|
||||
const newList = [...types.map(normalizeEventType)];
|
||||
const itemIndex = newList.findIndex((item) => item.id === id);
|
||||
if (itemIndex !== -1 && newList[itemIndex]) {
|
||||
newList[itemIndex].hidden = !newList[itemIndex].hidden;
|
||||
@@ -272,13 +289,13 @@ export const EventTypeList = ({
|
||||
});
|
||||
|
||||
async function moveEventType(index: number, increment: 1 | -1) {
|
||||
const newList = [...types];
|
||||
const newList = [...types.map(normalizeEventType)];
|
||||
|
||||
const type = types[index];
|
||||
const tmp = types[index + increment];
|
||||
if (tmp) {
|
||||
newList[index] = tmp;
|
||||
newList[index + increment] = type;
|
||||
newList[index] = normalizeEventType(tmp);
|
||||
newList[index + increment] = normalizeEventType(type);
|
||||
}
|
||||
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
@@ -306,7 +323,7 @@ export const EventTypeList = ({
|
||||
}
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
|
||||
const openDuplicateModal = (eventType: DeNormalizedEventType, group: DeNormalizedEventTypeGroup) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
|
||||
if (value) newSearchParams.set(key, value.toString());
|
||||
@@ -331,7 +348,7 @@ export const EventTypeList = ({
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
|
||||
if (previousValue) {
|
||||
const newList = types.filter((item) => item.id !== id);
|
||||
const newList = types.filter((item) => item.id !== id).map(normalizeEventType);
|
||||
|
||||
utils.viewer.eventTypes.getByViewer.setData(undefined, {
|
||||
...previousValue,
|
||||
@@ -390,7 +407,7 @@ export const EventTypeList = ({
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isPrivateURLEnabled = type.hashedLink?.link;
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
@@ -408,7 +425,7 @@ export const EventTypeList = ({
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && !isManagedEventType && (
|
||||
{!!type.teamId && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
@@ -522,7 +539,7 @@ export const EventTypeList = ({
|
||||
{!isManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EventTypeEmbedButton
|
||||
namespace={type.slug}
|
||||
namespace=""
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon={Code}
|
||||
@@ -821,7 +838,7 @@ const Actions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
|
||||
const EmptyEventTypeList = ({ group }: { group: DeNormalizedEventTypeGroup }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
@@ -843,7 +860,7 @@ const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
|
||||
const Main = ({
|
||||
status,
|
||||
errorMessage,
|
||||
data,
|
||||
data: rawData,
|
||||
filters,
|
||||
}: {
|
||||
status: string;
|
||||
@@ -854,7 +871,7 @@ const Main = ({
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
if (!data || status === "pending") {
|
||||
if (!rawData || status === "pending") {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
@@ -863,7 +880,11 @@ const Main = ({
|
||||
}
|
||||
|
||||
const isFilteredByOnlyOneItem =
|
||||
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
|
||||
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
|
||||
rawData.eventTypeGroups.length === 1;
|
||||
|
||||
const data = denormalizePayload(rawData);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
|
||||
@@ -871,7 +892,7 @@ const Main = ({
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
data.eventTypeGroups.map((group, index: number) => (
|
||||
<div
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
@@ -953,7 +974,10 @@ const EventTypesPage: React.FC & {
|
||||
}, [orgBranding, user]);
|
||||
|
||||
return (
|
||||
<ShellMain
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
withoutSeo
|
||||
heading={t("event_types_page_title")}
|
||||
hideHeadingOnMobile
|
||||
@@ -965,8 +989,39 @@ const EventTypesPage: React.FC & {
|
||||
description="Create events to share for people to book on your calendar."
|
||||
/>
|
||||
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
|
||||
</ShellMain>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypesPage;
|
||||
|
||||
function normalizeEventType(eventType: DeNormalizedEventType): EventType {
|
||||
return {
|
||||
...eventType,
|
||||
userIds: eventType.users.map((user) => user.id),
|
||||
};
|
||||
}
|
||||
|
||||
function denormalizePayload(data: NonNullable<GetByViewerResponse>) {
|
||||
return {
|
||||
...data,
|
||||
eventTypeGroups: data.eventTypeGroups.map((eventTypeGroup) => {
|
||||
return {
|
||||
...eventTypeGroup,
|
||||
eventTypes: eventTypeGroup.eventTypes.map((eventType) => {
|
||||
const { userIds, ...rest } = eventType;
|
||||
return {
|
||||
...rest,
|
||||
users: userIds.map((userId) => {
|
||||
const user = data.allUsersAcrossAllEventTypes.get(userId);
|
||||
if (!user) {
|
||||
throw new Error(`User with id ${userId} not found in allUsersAcrossAllEventTypes`);
|
||||
}
|
||||
return user;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -82,6 +82,8 @@ const EventWebhooksTab = dynamic(() =>
|
||||
|
||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||
|
||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||
|
||||
export type FormValues = {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -117,7 +119,7 @@ export type FormValues = {
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
@@ -139,13 +141,15 @@ export type FormValues = {
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
hosts: Host[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
secondaryEmailId?: number;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -189,14 +193,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
formMethods.setValue(
|
||||
"children",
|
||||
formMethods.getValues().children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}))
|
||||
);
|
||||
formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
|
||||
const currentValues = formMethods.getValues();
|
||||
|
||||
currentValues.children = currentValues.children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}));
|
||||
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
|
||||
|
||||
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
|
||||
formMethods.reset(currentValues);
|
||||
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
@@ -250,7 +257,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const defaultValues: any = useMemo(() => {
|
||||
return {
|
||||
@@ -261,7 +267,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||
eventName: eventType.eventName || "",
|
||||
scheduleName: eventType.scheduleName,
|
||||
periodDays: eventType.periodDays || 30,
|
||||
periodDays: eventType.periodDays,
|
||||
requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
|
||||
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||
seatsShowAttendees: eventType.seatsShowAttendees,
|
||||
@@ -287,7 +293,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
offsetStart: eventType.offsetStart,
|
||||
bookingFields: eventType.bookingFields,
|
||||
periodType: eventType.periodType,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
|
||||
schedulingType: eventType.schedulingType,
|
||||
requiresConfirmation: eventType.requiresConfirmation,
|
||||
slotInterval: eventType.slotInterval,
|
||||
@@ -296,6 +302,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
hosts: eventType.hosts,
|
||||
successRedirectUrl: eventType.successRedirectUrl || "",
|
||||
users: eventType.users,
|
||||
useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId: eventType?.secondaryEmailId || -1,
|
||||
children: eventType.children.map((ch) => ({
|
||||
...ch,
|
||||
created: true,
|
||||
@@ -312,7 +320,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(
|
||||
@@ -401,14 +408,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
.passthrough()
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValues]);
|
||||
const {
|
||||
formState: { isDirty: isFormDirty, dirtyFields },
|
||||
} = formMethods;
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
const availability = formMethods.watch("availability");
|
||||
@@ -421,10 +423,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
).length;
|
||||
}
|
||||
|
||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||
const permalink = `${WEBSITE_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||
eventType.slug
|
||||
}`;
|
||||
|
||||
const tabMap = {
|
||||
setup: (
|
||||
<EventSetupTab
|
||||
@@ -450,8 +451,100 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(value: T): boolean => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
const isFieldDirty = (fieldName: keyof FormValues) => {
|
||||
// If the field itself is directly marked as dirty
|
||||
if (dirtyFields[fieldName] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the field is an object or an array
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fieldValue: any = getNestedField(dirtyFields, fieldName);
|
||||
if (isObject(fieldValue)) {
|
||||
for (const key in fieldValue) {
|
||||
if (fieldValue[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isArray(fieldValue)) {
|
||||
for (const element of fieldValue) {
|
||||
// If element is an object, check each property of the object
|
||||
if (isObject(element)) {
|
||||
for (const key in element) {
|
||||
if (element[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(element[key]) || isArray(element[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays within each element
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getNestedField = (obj: typeof dirtyFields, path: string) => {
|
||||
const keys = path.split(".");
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
// @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
|
||||
const currentKey = current[keys[i]];
|
||||
if (currentKey === undefined) return undefined;
|
||||
current = currentKey;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
Object.keys(dirtyFields).forEach((key) => {
|
||||
const typedKey = key as keyof typeof dirtyFields;
|
||||
updatedFields[typedKey] = undefined;
|
||||
const isDirty = isFieldDirty(typedKey);
|
||||
if (isDirty) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
updatedFields[typedKey] = values[typedKey];
|
||||
}
|
||||
});
|
||||
return updatedFields;
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -467,7 +560,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -480,8 +572,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
} = dirtyValues;
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
@@ -501,8 +592,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
// if length is unchanged, we skip this check
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,14 +613,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -539,7 +634,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
};
|
||||
|
||||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
@@ -592,6 +699,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -614,9 +724,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
} = dirtyValues;
|
||||
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
@@ -635,8 +745,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -648,14 +761,15 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -667,7 +781,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -206,7 +207,10 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username || "",
|
||||
image: user.avatar,
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
|
||||
@@ -155,7 +155,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
|
||||
+11
-1
@@ -207,7 +207,17 @@ const nextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
webpack: (config, { webpack, buildId }) => {
|
||||
webpack: (config, { webpack, buildId, isServer }) => {
|
||||
if (isServer) {
|
||||
// Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp:
|
||||
/(^@google-cloud\/spanner|^@mongodb-js\/zstd|^@sap\/hana-client\/extension\/Stream$|^@sap\/hana-client|^@sap\/hana-client$|^aws-crt|^aws4$|^better-sqlite3$|^bson-ext$|^cardinal$|^cloudflare:sockets$|^hdb-pool$|^ioredis$|^kerberos$|^mongodb-client-encryption$|^mysql$|^oracledb$|^pg-native$|^pg-query-stream$|^react-native-sqlite-storage$|^snappy\/package\.json$|^snappy$|^sql.js$|^sqlite3$|^typeorm-aurora-data-api-driver$)/,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
config.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.7.12",
|
||||
"version": "3.8.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -24,7 +24,7 @@
|
||||
"yarn": "3.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "1.8.2",
|
||||
"@boxyhq/saml-jackson": "1.18.6",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/app-store-cli": "*",
|
||||
"@calcom/core": "*",
|
||||
@@ -130,7 +130,7 @@
|
||||
"tailwindcss-radix": "^2.6.0",
|
||||
"turndown": "^7.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
|
||||
const currentPassword = user.password;
|
||||
const currentPassword = user.password?.hash;
|
||||
if (!currentPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
@@ -53,12 +53,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
await prisma.user.update({
|
||||
await prisma.userPassword.upsert({
|
||||
where: {
|
||||
id: user.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
create: {
|
||||
hash: hashedPassword,
|
||||
userId: user.id,
|
||||
},
|
||||
update: {
|
||||
hash: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
email: maybeRequest.email,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -41,7 +41,7 @@ async function handler(req: NextApiRequest) {
|
||||
data: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
role: "ADMIN",
|
||||
name: parsedQuery.data.full_name,
|
||||
emailVerified: new Date(),
|
||||
|
||||
@@ -23,13 +23,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
if (user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
@@ -35,7 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
if (!user.password?.hash) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import { z } from "zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const verifySchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token } = verifySchema.parse(req.query);
|
||||
|
||||
@@ -26,23 +29,119 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "Token expired" });
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
// The user is verifying the secondary email
|
||||
if (foundToken?.secondaryEmailId) {
|
||||
await prisma.secondaryEmail.update({
|
||||
where: {
|
||||
id: foundToken.secondaryEmailId,
|
||||
email: foundToken?.identifier,
|
||||
},
|
||||
data: {
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await cleanUpVerificationTokens(foundToken.id);
|
||||
|
||||
return res.redirect(`${WEBAPP_URL}/settings/my-account/profile`);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: foundToken?.identifier,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: "Cannot find a user attached to this token" });
|
||||
}
|
||||
|
||||
const userMetadataParsed = userMetadata.parse(user.metadata);
|
||||
// Attach the new email and verify
|
||||
if (userMetadataParsed?.emailChangeWaitingForVerification) {
|
||||
// Ensure this email isnt in use
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: userMetadataParsed?.emailChangeWaitingForVerification },
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
|
||||
}
|
||||
|
||||
// Ensure this email isn't being added by another user as secondary email
|
||||
const existingSecondaryUser = await prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
email: userMetadataParsed?.emailChangeWaitingForVerification,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSecondaryUser && existingSecondaryUser.userId !== user.id) {
|
||||
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
|
||||
}
|
||||
|
||||
const oldEmail = user.email;
|
||||
const updatedEmail = userMetadataParsed.emailChangeWaitingForVerification;
|
||||
delete userMetadataParsed.emailChangeWaitingForVerification;
|
||||
|
||||
// Update and re-verify
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
email: updatedEmail,
|
||||
metadata: userMetadataParsed,
|
||||
},
|
||||
});
|
||||
|
||||
// The user is trying to update the email to an already existing unverified secondary email of his
|
||||
// so we swap the emails and its verified status
|
||||
if (existingSecondaryUser?.userId === user.id) {
|
||||
await prisma.secondaryEmail.update({
|
||||
where: {
|
||||
id: existingSecondaryUser.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
email: oldEmail,
|
||||
emailVerified: user.emailVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await cleanUpVerificationTokens(foundToken.id);
|
||||
|
||||
return res.status(200).json({
|
||||
updatedEmail,
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const hasCompletedOnboarding = user.completedOnboarding;
|
||||
|
||||
return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
|
||||
}
|
||||
|
||||
export async function cleanUpVerificationTokens(id: number) {
|
||||
// Delete token from DB after it has been used
|
||||
await prisma.verificationToken.delete({
|
||||
where: {
|
||||
id: foundToken?.id,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const hasCompletedOnboarding = user.completedOnboarding;
|
||||
|
||||
res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const booking = await handleNewBooking(req, {
|
||||
isNotAnApiCall: true,
|
||||
});
|
||||
const booking = await handleNewBooking(req);
|
||||
return booking;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import type { BookingResponse, RecurringBookingCreateBody } from "@calcom/features/bookings/types";
|
||||
import { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleNewRecurringBooking";
|
||||
import type { BookingResponse } from "@calcom/features/bookings/types";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
// @TODO: Didn't look at the contents of this function in order to not break old booking page.
|
||||
|
||||
@@ -17,74 +16,12 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
||||
rateLimitingType: "core",
|
||||
identifier: userIp,
|
||||
});
|
||||
const data: RecurringBookingCreateBody[] = req.body;
|
||||
const session = await getServerSession({ req, res });
|
||||
const createdBookings: BookingResponse[] = [];
|
||||
const allRecurringDates: { start: string | undefined; end: string | undefined }[] = data.map((booking) => {
|
||||
return { start: booking.start, end: booking.end };
|
||||
});
|
||||
const appsStatus: AppsStatus[] | undefined = undefined;
|
||||
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const numSlotsToCheckForAvailability = 2;
|
||||
|
||||
let thirdPartyRecurringEventId = null;
|
||||
for (let key = 0; key < data.length; key++) {
|
||||
const booking = data[key];
|
||||
// Disable AppStatus in Recurring Booking Email as it requires us to iterate backwards to be able to compute the AppsStatus for all the bookings except the very first slot and then send that slot's email with statuses
|
||||
// It is also doubtful that how useful is to have the AppsStatus of all the bookings in the email.
|
||||
// It is more important to iterate forward and check for conflicts for only first few bookings defined by 'numSlotsToCheckForAvailability'
|
||||
// if (key === 0) {
|
||||
// const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
|
||||
// .flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
|
||||
// .reduce((prev, curr) => {
|
||||
// if (prev[curr.type]) {
|
||||
// prev[curr.type].failures += curr.failures;
|
||||
// prev[curr.type].success += curr.success;
|
||||
// } else {
|
||||
// prev[curr.type] = curr;
|
||||
// }
|
||||
// return prev;
|
||||
// }, {} as { [key: string]: AppsStatus });
|
||||
// appsStatus = Object.values(calcAppsStatus);
|
||||
// }
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(req);
|
||||
|
||||
const recurringEventReq: NextApiRequest & { userId?: number } = req;
|
||||
|
||||
recurringEventReq.body = {
|
||||
...booking,
|
||||
appsStatus,
|
||||
allRecurringDates,
|
||||
isFirstRecurringSlot: key == 0,
|
||||
thirdPartyRecurringEventId,
|
||||
numSlotsToCheckForAvailability,
|
||||
currentRecurringIndex: key,
|
||||
noEmail: key !== 0,
|
||||
};
|
||||
|
||||
const promiseEachRecurringBooking: ReturnType<typeof handleNewBooking> = handleNewBooking(
|
||||
recurringEventReq,
|
||||
{
|
||||
isNotAnApiCall: true,
|
||||
}
|
||||
);
|
||||
|
||||
const eachRecurringBooking = await promiseEachRecurringBooking;
|
||||
|
||||
createdBookings.push(eachRecurringBooking);
|
||||
|
||||
if (!thirdPartyRecurringEventId) {
|
||||
if (eachRecurringBooking.references && eachRecurringBooking.references.length > 0) {
|
||||
for (const reference of eachRecurringBooking.references!) {
|
||||
if (reference.thirdPartyRecurringEventId) {
|
||||
thirdPartyRecurringEventId = reference.thirdPartyRecurringEventId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return createdBookings;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: booking?.userPrimaryEmail ?? user.email,
|
||||
name,
|
||||
timeZone: user.timeZone,
|
||||
language: { translate: tOrganizer, locale: user.locale ?? "en" },
|
||||
|
||||
@@ -230,7 +230,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "Email-less",
|
||||
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
|
||||
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
// This is the handler for the SCIM API requests
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { dsyncController } = await jackson();
|
||||
|
||||
const { method, query, body } = req;
|
||||
|
||||
const [directoryId, path, resourceId] = query.directory as string[];
|
||||
|
||||
// Handle the SCIM API requests
|
||||
const request: DirectorySyncRequest = {
|
||||
method: method as string,
|
||||
directoryId,
|
||||
resourceId,
|
||||
apiSecret: extractAuthToken(req),
|
||||
resourceType: path === "Users" ? "users" : "groups",
|
||||
body: body ? JSON.parse(body) : undefined,
|
||||
query: {
|
||||
count: req.query.count ? parseInt(req.query.count as string) : undefined,
|
||||
startIndex: req.query.startIndex ? parseInt(req.query.startIndex as string) : undefined,
|
||||
filter: req.query.filter as string,
|
||||
},
|
||||
};
|
||||
|
||||
const { status, data } = await dsyncController.requests.handle(request, handleEvents);
|
||||
|
||||
res.status(status).json(data);
|
||||
}
|
||||
|
||||
// Fetch the auth token from the request headers
|
||||
export const extractAuthToken = (req: NextApiRequest): string | null => {
|
||||
const authHeader = req.headers.authorization || null;
|
||||
|
||||
return authHeader ? authHeader.split(" ")[1] : null;
|
||||
};
|
||||
|
||||
// Handle the SCIM events
|
||||
const handleEvents = async (event: DirectorySyncEvent) => {
|
||||
const dSyncData = await prisma.dSyncData.findFirst({
|
||||
where: {
|
||||
directoryId: event.directory_id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dSyncData) {
|
||||
throw new Error("Directory sync data not found");
|
||||
}
|
||||
|
||||
const { organizationId } = dSyncData;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
|
||||
}
|
||||
|
||||
if (event.event.includes("group")) {
|
||||
handleGroupEvents(event, organizationId);
|
||||
}
|
||||
|
||||
if (event.event === "user.created" || event.event === "user.updated") {
|
||||
await handleUserEvents(event, organizationId);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { dsyncRouter } from "@calcom/trpc/server/routers/viewer/dsync/_router";
|
||||
|
||||
export default createNextApiHandler(dsyncRouter);
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -19,11 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(403).json({ message: "Credential sharing is not enabled" });
|
||||
}
|
||||
|
||||
if (
|
||||
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
|
||||
process.env.CALCOM_WEBHOOK_SECRET
|
||||
) {
|
||||
return res.status(403).json({ message: "Invalid webhook secret" });
|
||||
if (req.headers[CREDENTIAL_SYNC_SECRET_HEADER_NAME] !== CREDENTIAL_SYNC_SECRET) {
|
||||
return res.status(403).json({ message: "Invalid credential sync secret" });
|
||||
}
|
||||
|
||||
const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
|
||||
|
||||
@@ -6,9 +6,12 @@ import type { GetStaticPaths } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
@@ -17,6 +20,12 @@ import App from "@components/apps/App";
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
|
||||
function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
|
||||
const { error, setQuery: setError } = useRouterQuery("error");
|
||||
const { t } = useLocale();
|
||||
if (error === "account_already_linked") {
|
||||
showToast(t(error), "error", { id: error });
|
||||
setError(undefined);
|
||||
}
|
||||
// If it's not production environment, it would be a better idea to inform that the App is disabled.
|
||||
if (props.isAppDisabled) {
|
||||
if (!IS_PRODUCTION) {
|
||||
|
||||
@@ -26,6 +26,8 @@ export default function Apps({ apps }: PageProps) {
|
||||
<Shell
|
||||
isPublic
|
||||
backPath="/apps"
|
||||
title="Apps Store"
|
||||
description="Connecting people, technology and the workplace."
|
||||
smallHeading
|
||||
heading={
|
||||
<>
|
||||
@@ -45,7 +47,7 @@ export default function Apps({ apps }: PageProps) {
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps
|
||||
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
?.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
|
||||
@@ -16,7 +16,12 @@ export default function Apps({ categories }: Omit<inferSSRProps<typeof getServer
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell isPublic large hideHeadingOnMobile>
|
||||
<Shell
|
||||
isPublic
|
||||
large
|
||||
hideHeadingOnMobile
|
||||
title="Apps Store"
|
||||
description="Connecting people, technology and the workplace.">
|
||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||
<Link
|
||||
href="/apps"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -103,4 +103,14 @@ export default function Apps({
|
||||
export { getServerSideProps };
|
||||
|
||||
Apps.PageWrapper = PageWrapper;
|
||||
Apps.getLayout = getLayout;
|
||||
Apps.getLayout = (page: React.ReactElement) => {
|
||||
return (
|
||||
<Shell
|
||||
title="Apps Store"
|
||||
description="Create forms to direct attendees to the correct destinations."
|
||||
withoutMain={true}
|
||||
hideHeadingOnMobile>
|
||||
{page}
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user