Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fe5fe8d53b | ||
|
|
0cc5937104 | ||
|
|
fde3b49126 | ||
|
|
556787b93b | ||
|
|
dcb244d7be | ||
|
|
8d7fb75f71 | ||
|
|
4fe75771b8 | ||
|
|
63f537a08f | ||
|
|
bed593c835 | ||
|
|
f367c5ed3d | ||
|
|
af1382c5ba | ||
|
|
eeb0fc795d | ||
|
|
4aa040d96e | ||
|
|
12466fb064 | ||
|
|
42a3448abb | ||
|
|
92ff49f6a1 | ||
|
|
c4aa555610 | ||
|
|
a45d77de51 | ||
|
|
0421dd401a | ||
|
|
094189fc3b | ||
|
|
677a964d4f | ||
|
|
2357c919c1 | ||
|
|
27b7cccfa6 | ||
|
|
ab2354f2a4 | ||
|
|
538e5017c5 | ||
|
|
2e3060b738 | ||
|
|
e9d9aebd88 | ||
|
|
7d1c69801e | ||
|
|
4235b32349 | ||
|
|
2ca031e25c | ||
|
|
9f734b8964 | ||
|
|
d6927c5049 | ||
|
|
910f5f906f | ||
|
|
71f955e256 | ||
|
|
5e60654aa9 | ||
|
|
b6175b3dde | ||
|
|
a0a731b357 | ||
|
|
fdaa6961f8 | ||
|
|
d91e74ec13 | ||
|
|
0af780aac9 | ||
|
|
41b0589ab0 | ||
|
|
00e96ac7ad | ||
|
|
63feb721ea | ||
|
|
43c1bd63ca | ||
|
|
8bc265faaa | ||
|
|
f33ea91ab0 | ||
|
|
cc0ff17d97 | ||
|
|
4d128971bb | ||
|
|
f9f82283fb | ||
|
|
111135a605 | ||
|
|
04e577582d | ||
|
|
6643d3c20b | ||
|
|
3c42301c19 | ||
|
|
20517be280 | ||
|
|
fa45a85088 | ||
|
|
c839e40a3e | ||
|
|
9422e28598 | ||
|
|
d804b3947d | ||
|
|
3fa710fc1c | ||
|
|
f9359aaa7b | ||
|
|
7c7ca79d48 | ||
|
|
753cb5803d | ||
|
|
210bc9fc8c | ||
|
|
3d8b5390df | ||
|
|
a477f337dc | ||
|
|
9db9204088 | ||
|
|
fe46b81c63 | ||
|
|
17db644be1 | ||
|
|
e4037be79a | ||
|
|
ebe6a60366 | ||
|
|
fa9c6ea0ef | ||
|
|
9b22f44cba | ||
|
|
c84f75df99 | ||
|
|
8f9a4fc78f | ||
|
|
7b30ade284 | ||
|
|
24a4646fdc | ||
|
|
0860036607 | ||
|
|
d080a0ef2d | ||
|
|
a6eab74fd1 | ||
|
|
0a2e275908 | ||
|
|
3b05ebc5b5 | ||
|
|
e5b4d2fb80 | ||
|
|
00a2a3d8a8 | ||
|
|
30ba168fc3 | ||
|
|
12eec186b2 | ||
|
|
4417d649ea | ||
|
|
7058da70e6 | ||
|
|
2cb383c337 |
+7
-2
@@ -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
|
||||
@@ -236,6 +239,8 @@ CSP_POLICY=
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
||||
|
||||
# Control time intervals on a user's Schedule availability
|
||||
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Contact: security@cal.com
|
||||
Preferred-Languages: en
|
||||
Canonical: https://cal.com/.well-known/security.txt
|
||||
Policy: https://github.com/calcom/cal.com/security/policy
|
||||
@@ -0,0 +1,8 @@
|
||||
diff --git a/plugin/timezone.js b/plugin/timezone.js
|
||||
index fb6112a96f03f53ba78162ac323dce17f635161d..991e4e5410a32f5a69cb6e0e9476a9c529657aae 100644
|
||||
--- a/plugin/timezone.js
|
||||
+++ b/plugin/timezone.js
|
||||
@@ -1 +1 @@
|
||||
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||"short",o=t+"|"+i,r=e[o];return r||(r=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,v=r[0]+"-"+r[1]+"-"+r[2]+" "+l+":"+r[4]+":"+r[5]+":000",h=+e;return(o.utc(v).valueOf()-(h-=h%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n=this.utcOffset(),i=this.toDate(),a=i.toLocaleString("en-US",{timeZone:t}),u=Math.round((i-new Date(a))/1e3/60),f=o(a).$set("millisecond",this.$ms).utcOffset(15*-Math.round(i.getTimezoneOffset()/15)-u,!0);if(e){var s=f.utcOffset();f=f.add(n-s,"minute")}return f.$x.$timezone=t,f},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if("string"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));
|
||||
\ No newline at end of file
|
||||
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";function t(){return t=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t},t.apply(this,arguments)}var e={year:0,month:1,day:2,hour:3,minute:4,second:5},n={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"},i={},r={};return function(o,a,u){var f,s=function(e,r,o){void 0===o&&(o={});var a=new Date(e),u=function(e,r){void 0===r&&(r={});var o=r.timeZoneName||"short",a=e+"|"+o,u=i[a];return u||(u=new Intl.DateTimeFormat("en-US",t({},n,{hour12:!1,timeZone:e,timeZoneName:o})),i[a]=u),u}(r,o);return u.formatToParts(a)},m=function(t,n){for(var i=s(t,n),r=[],o=0;o<i.length;o+=1){var a=i[o],f=a.type,m=a.value,c=e[f];c>=0&&(r[c]=parseInt(m,10))}var l=r[3],d=24===l?0:l,h=r[0]+"-"+r[1]+"-"+r[2]+" "+d+":"+r[4]+":"+r[5]+":000",v=+t;return(u.utc(h).valueOf()-(v-=v%1e3))/6e4},c=a.prototype;c.tz=function(e,i){void 0===e&&(e=f);var o=this.utcOffset(),a=this.toDate(),s=function(e){var i=r[e];return i||(i=new Intl.DateTimeFormat("en-US",t({},n,{timeZone:e})),r[e]=i),i}(e).format(a),m=Math.round((a-new Date(s))/1e3/60),c=u(s,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(15*-Math.round(a.getTimezoneOffset()/15)-m,!0);if(i){var l=c.utcOffset();c=c.add(o-l,"minute")}return c.$x.$timezone=e,c},c.offsetName=function(t){var e=this.$x.$timezone||u.tz.guess(),n=s(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var l=c.startOf;c.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return l.call(this,t,e);var n=u(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return l.call(n,t,e).tz(this.$x.$timezone,!0)},u.tz=function(t,e,n){var i=n&&e,r=n||e||f,o=m(+u(),r);if("string"!=typeof t)return u(t).tz(r);var a=function(t,e,n){var i=t-60*e*1e3,r=m(i,n);if(e===r)return[i,e];var o=m(i-=60*(r-e)*1e3,n);return r===o?[i,r]:[t-60*Math.min(r,o)*1e3,Math.max(r,o)]}(u.utc(t,i).valueOf(),o,r),s=a[0],c=a[1],l=u(s).utcOffset(c);return l.$x.$timezone=r,l},u.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},u.tz.setDefault=function(t){f=t}}}));
|
||||
@@ -0,0 +1,8 @@
|
||||
diff --git a/plugin/timezone.js b/plugin/timezone.js
|
||||
index fb6112a96f03f53ba78162ac323dce17f635161d..991e4e5410a32f5a69cb6e0e9476a9c529657aae 100644
|
||||
--- a/plugin/timezone.js
|
||||
+++ b/plugin/timezone.js
|
||||
@@ -1 +1 @@
|
||||
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||"short",o=t+"|"+i,r=e[o];return r||(r=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,v=r[0]+"-"+r[1]+"-"+r[2]+" "+l+":"+r[4]+":"+r[5]+":000",h=+e;return(o.utc(v).valueOf()-(h-=h%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n=this.utcOffset(),i=this.toDate(),a=i.toLocaleString("en-US",{timeZone:t}),u=Math.round((i-new Date(a))/1e3/60),f=o(a).$set("millisecond",this.$ms).utcOffset(15*-Math.round(i.getTimezoneOffset()/15)-u,!0);if(e){var s=f.utcOffset();f=f.add(n-s,"minute")}return f.$x.$timezone=t,f},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if("string"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));
|
||||
\ No newline at end of file
|
||||
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){"use strict";function t(){return t=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t},t.apply(this,arguments)}var e={year:0,month:1,day:2,hour:3,minute:4,second:5},n={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"},i={},r={};return function(o,a,u){var f,s=function(e,r,o){void 0===o&&(o={});var a=new Date(e),u=function(e,r){void 0===r&&(r={});var o=r.timeZoneName||"short",a=e+"|"+o,u=i[a];return u||(u=new Intl.DateTimeFormat("en-US",t({},n,{hour12:!1,timeZone:e,timeZoneName:o})),i[a]=u),u}(r,o);return u.formatToParts(a)},m=function(t,n){for(var i=s(t,n),r=[],o=0;o<i.length;o+=1){var a=i[o],f=a.type,m=a.value,c=e[f];c>=0&&(r[c]=parseInt(m,10))}var l=r[3],d=24===l?0:l,h=r[0]+"-"+r[1]+"-"+r[2]+" "+d+":"+r[4]+":"+r[5]+":000",v=+t;return(u.utc(h).valueOf()-(v-=v%1e3))/6e4},c=a.prototype;c.tz=function(e,i){void 0===e&&(e=f);var o=this.utcOffset(),a=this.toDate(),s=function(e){var i=r[e];return i||(i=new Intl.DateTimeFormat("en-US",t({},n,{timeZone:e})),r[e]=i),i}(e).format(a),m=Math.round((a-new Date(s))/1e3/60),c=u(s,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(15*-Math.round(a.getTimezoneOffset()/15)-m,!0);if(i){var l=c.utcOffset();c=c.add(o-l,"minute")}return c.$x.$timezone=e,c},c.offsetName=function(t){var e=this.$x.$timezone||u.tz.guess(),n=s(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var l=c.startOf;c.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return l.call(this,t,e);var n=u(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return l.call(n,t,e).tz(this.$x.$timezone,!0)},u.tz=function(t,e,n){var i=n&&e,r=n||e||f,o=m(+u(),r);if("string"!=typeof t)return u(t).tz(r);var a=function(t,e,n){var i=t-60*e*1e3,r=m(i,n);if(e===r)return[i,e];var o=m(i-=60*(r-e)*1e3,n);return r===o?[i,r]:[t-60*Math.min(r,o)*1e3,Math.max(r,o)]}(u.utc(t,i).valueOf(),o,r),s=a[0],c=a[1],l=u(s).utcOffset(c);return l.$x.$timezone=r,l},u.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},u.tz.setDefault=function(t){f=t}}}));
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { initializeAgentExecutorWithOptions } from "langchain/agents";
|
||||
import { ChatOpenAI } from "langchain/chat_models/openai";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import createBookingIfAvailable from "../tools/createBooking";
|
||||
@@ -13,7 +13,7 @@ import type { User, UserList } from "../types/user";
|
||||
import type { WorkingHours } from "../types/workingHours";
|
||||
import now from "./now";
|
||||
|
||||
const gptModel = "gpt-4-0613";
|
||||
const gptModel = "gpt-4-0125-preview";
|
||||
|
||||
/**
|
||||
* Core of the Cal.ai booking agent: a LangChain Agent Executor.
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/types/window.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { captureException as SentryCaptureException } from "@sentry/nextjs";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
@@ -9,7 +9,7 @@ export const captureErrors: NextMiddleware = async (_req, res, next) => {
|
||||
// middleware and the API route handler
|
||||
await next();
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
SentryCaptureException(error);
|
||||
const redactedError = redactError(error);
|
||||
if (redactedError instanceof Error) {
|
||||
res.status(400).json({ message: redactedError.message, error: redactedError });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
@@ -9,6 +10,11 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
await checkRateLimitAndThrowError({
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
});
|
||||
|
||||
await next();
|
||||
|
||||
@@ -62,6 +62,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
assignAllTeamMembers: true,
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
export const schemaQuerySlug = baseApiParams.extend({
|
||||
slug: z.string().optional(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import tzdata from "tzdata";
|
||||
import * as z from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
|
||||
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
import getCalLink from "./_utils/getCalLink";
|
||||
@@ -22,6 +23,12 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* - in: query
|
||||
* name: slug
|
||||
* schema:
|
||||
* type: string
|
||||
* required: false
|
||||
* description: Slug to filter event types by
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
@@ -35,16 +42,20 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, prisma } = req;
|
||||
const { userId, prisma, isAdmin } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
const { slug } = schemaQuerySlug.parse(req.query);
|
||||
const shouldUseUserId = !isAdmin || !slug || !!req.query.userId;
|
||||
// When user is admin and no query params are provided we should return all event types.
|
||||
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
|
||||
const data = await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
userId: shouldUseUserId ? { in: userIds } : undefined,
|
||||
slug: slug, // slug will be undefined if not provided in query
|
||||
},
|
||||
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
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
|
||||
import { ZInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const data = ZInviteMemberInputSchema.parse(req.body);
|
||||
await checkPermissions(req, data);
|
||||
|
||||
async function sessionGetter() {
|
||||
return {
|
||||
user: {
|
||||
id: req.userId,
|
||||
username: "",
|
||||
profile: {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
username: "",
|
||||
upId: "",
|
||||
} satisfies UserProfile,
|
||||
},
|
||||
hasValidLicense: true,
|
||||
expires: "",
|
||||
upId: "",
|
||||
};
|
||||
}
|
||||
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
try {
|
||||
const caller = viewerTeamsRouter.createCaller(ctx);
|
||||
await caller.inviteMember({
|
||||
role: data.role,
|
||||
language: data.language,
|
||||
teamId: data.teamId,
|
||||
usernameOrEmail: data.usernameOrEmail,
|
||||
isOrg: data.isOrg,
|
||||
});
|
||||
|
||||
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
throw new HttpError({ statusCode, message: cause.message });
|
||||
}
|
||||
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
if (isAdmin) return;
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } },
|
||||
});
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,98 @@
|
||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import handler from "./_get";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
function buildMockData() {
|
||||
prismock.user.create({
|
||||
data: {
|
||||
id: 1,
|
||||
username: "test",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
},
|
||||
});
|
||||
prismock.eventType.create({
|
||||
data: {
|
||||
id: 1,
|
||||
slug: "test",
|
||||
length: 30,
|
||||
title: "Test Event Type",
|
||||
userId: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("GET /api/slots", () => {
|
||||
describe("Errors", () => {
|
||||
test("Missing required data", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", () => {
|
||||
describe("Regular event-type", () => {
|
||||
test("Returns and event type available slots", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
eventTypeId: 1,
|
||||
startTime: dayjs().format(),
|
||||
endTime: dayjs().add(1, "day").format(),
|
||||
usernameList: "test",
|
||||
},
|
||||
prisma: prismock,
|
||||
});
|
||||
buildMockData();
|
||||
await handler(req, res);
|
||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"slots": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
test("Returns and event type available slots with passed timeZone", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
eventTypeId: 1,
|
||||
startTime: dayjs().format(),
|
||||
endTime: dayjs().add(1, "day").format(),
|
||||
usernameList: "test",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
prisma: prismock,
|
||||
});
|
||||
buildMockData();
|
||||
await handler(req, res);
|
||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"slots": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
{}
|
||||
)
|
||||
: availableSlots;
|
||||
: availableSlots.slots;
|
||||
|
||||
return { slots: slotsInProvidedTimeZone };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
@@ -21,9 +22,17 @@ const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
id: req.userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: req.isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
|
||||
profile: {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
username: "",
|
||||
upId: "",
|
||||
} satisfies UserProfile,
|
||||
},
|
||||
hasValidLicense: true /* To comply with TS signature */,
|
||||
expires: "" /* Not used in this context */,
|
||||
upId: "",
|
||||
};
|
||||
}
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
const { prisma, query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
await prisma.webhook.delete({ where: { id } });
|
||||
return { message: `Schedule with id: ${id} deleted successfully` };
|
||||
return { message: `Webhook with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { init as SentryInit } from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
SentryInit({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { init as SentryInit } from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
SentryInit({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
@@ -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,6 +3,8 @@ import type { NextMiddleware, NextRequest } from "next/server";
|
||||
import { NextResponse, URLPattern } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants";
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||
@@ -25,9 +27,6 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
enabled,
|
||||
]);
|
||||
|
||||
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
|
||||
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
|
||||
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
|
||||
|
||||
export const abTestMiddlewareFactory =
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
|
||||
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { DataTransformer } from "@trpc/server";
|
||||
type DataTransformer = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serialize: (data: any) => any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deserialize: (data: any) => any;
|
||||
};
|
||||
|
||||
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
||||
export function createHydrateClient(opts: { transformer: DataTransformer }) {
|
||||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
||||
const { state, children } = props;
|
||||
|
||||
@@ -16,6 +21,6 @@ export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
||||
return state;
|
||||
}, [state]);
|
||||
|
||||
return <Hydrate state={transformedState}>{children}</Hydrate>;
|
||||
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import { trpc } from "app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||
import { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
|
||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
+22
-1
@@ -1,12 +1,33 @@
|
||||
import { type TFunction } from "i18next";
|
||||
import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
import { getFixedT } from "@calcom/lib/server/getFixedT";
|
||||
//@ts-expect-error no type definitions
|
||||
import config from "@calcom/web/next-i18next.config";
|
||||
|
||||
import { preparePageMetadata } from "@lib/metadata";
|
||||
|
||||
const create = async (locale: string, ns: string) => {
|
||||
const { _nextI18Next } = await serverSideTranslations(locale, [ns], config);
|
||||
|
||||
const _i18n = i18next.createInstance();
|
||||
_i18n.init({
|
||||
lng: locale,
|
||||
resources: _nextI18Next?.initialI18nStore,
|
||||
fallbackLng: _nextI18Next?.userConfig?.i18n.defaultLocale,
|
||||
});
|
||||
return _i18n;
|
||||
};
|
||||
|
||||
const getFixedT = async (locale: string, ns: string) => {
|
||||
const i18n = await create(locale, ns);
|
||||
|
||||
return i18n.getFixedT(locale, ns);
|
||||
};
|
||||
|
||||
export const _generateMetadata = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import LegacyPage from "~/users/views/users-type-public-view";
|
||||
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import LegacyPage from "~/users/views/users-type-public-view";
|
||||
import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Record<string, string | string[]>;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
|
||||
const { eventData, booking, user, slug } = props;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user, eventSlug: slug, isTeamEvent: false, org: eventData.entity.orgSlug ?? null },
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
getData,
|
||||
Page: LegacyPage,
|
||||
getLayout: null,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Record<string, string | string[]>;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
|
||||
const { profile, markdownStrippedBio } = props;
|
||||
return await _generateMetadata(
|
||||
() => profile.name,
|
||||
() => markdownStrippedBio
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<UserPageProps>(getServerSideProps);
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,12 @@
|
||||
import Setup from "@pages/auth/setup";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: Setup,
|
||||
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
})<"P">;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type PageProps, getServerSideProps } from "@pages/booking/[uid]";
|
||||
import OldPage from "@pages/booking/[uid]/embed";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import OldPage from "~/bookings/views/bookings-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import OldPage, { type PageProps } from "@pages/booking/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
@@ -8,9 +7,11 @@ import { cookies, headers } from "next/headers";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import OldPage from "~/bookings/views/bookings-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Page from "@pages/bookings/[status]";
|
||||
import { withAppDirSsg } from "app/WithAppDirSsg";
|
||||
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 { getStaticProps } from "@lib/bookings/[status]/getStaticProps";
|
||||
|
||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import Page from "~/bookings/views/bookings-listing-view";
|
||||
import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
|
||||
|
||||
type Y = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
const getData = withAppDirSsg<Y>(getStaticProps);
|
||||
@@ -24,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";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
|
||||
import { getServerSideProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const { eventType } = await getData(legacyCtx);
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${eventType.title} | ${t("event_type")} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: EventTypePageWrapper })<"P">;
|
||||
@@ -1,8 +1,7 @@
|
||||
import EventTypes from "@pages/event-types";
|
||||
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 () =>
|
||||
await _generateMetadata(
|
||||
@@ -10,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">;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import LegacyPage, { getServerSideProps } from "@pages/insights/index";
|
||||
import LegacyPage from "@pages/insights/index";
|
||||
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";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -15,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 });
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type PageProps } from "@pages/team/[slug]";
|
||||
import EmbedPage from "@pages/team/[slug]/embed";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
return await _generateMetadata(
|
||||
() => teamName,
|
||||
() => teamName
|
||||
);
|
||||
};
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({
|
||||
Page: EmbedPage,
|
||||
getData: getEmbedData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,37 @@
|
||||
import TeamPage, { type PageProps } from "@pages/team/[slug]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
return await _generateMetadata(
|
||||
() => teamName,
|
||||
() => teamName
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,44 @@
|
||||
import TypePage, { type PageProps } from "@pages/team/[slug]/[type]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
|
||||
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 },
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TypePage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -0,0 +1,37 @@
|
||||
import TeamPage, { type PageProps } from "@pages/team/[slug]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { Params, SearchParams } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
}) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
const teamName = props.team.name || "Nameless Team";
|
||||
|
||||
return await _generateMetadata(
|
||||
() => teamName,
|
||||
() => teamName
|
||||
);
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({
|
||||
Page: TeamPage,
|
||||
getData,
|
||||
getLayout: null,
|
||||
isBookingPage: true,
|
||||
})<"P">;
|
||||
@@ -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,11 +1,11 @@
|
||||
import Page, { type JoinCallPageProps } from "@pages/video/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
|
||||
import VideosSingleView from "~/videos/views/videos-single-view";
|
||||
import { getServerSideProps, type PageProps } from "~/videos/views/videos-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -13,6 +13,6 @@ export const generateMetadata = async () =>
|
||||
(t) => t("quick_video_meeting")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr<JoinCallPageProps>(getServerSideProps);
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getData, Page, getLayout: null })<"P">;
|
||||
export default WithLayout({ getData, Page: VideosSingleView, getLayout: null })<"P">;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Page from "@pages/video/meeting-ended/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/video/meeting-ended/[uid]/getServerSideProps";
|
||||
import MeetingEnded from "~/videos/views/videos-meeting-ended-single-view";
|
||||
import { getServerSideProps } from "~/videos/views/videos-meeting-ended-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -13,4 +13,4 @@ export const generateMetadata = async () =>
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getData, Page, getLayout: null })<"P">;
|
||||
export default WithLayout({ getData, Page: MeetingEnded, getLayout: null })<"P">;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Page from "@pages/video/meeting-not-started/[uid]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
@@ -6,7 +5,8 @@ import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
|
||||
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
|
||||
import MeetingNotStarted from "~/videos/views/videos-meeting-not-started-single-view";
|
||||
import { getServerSideProps } from "~/videos/views/videos-meeting-not-started-single-view.getServerSideProps";
|
||||
|
||||
export const generateMetadata = async ({ params }: PageProps) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
@@ -24,4 +24,4 @@ export const generateMetadata = async ({ params }: PageProps) => {
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getData, Page, getLayout: null })<"P">;
|
||||
export default WithLayout({ getData, Page: MeetingNotStarted, getLayout: null })<"P">;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import LegacyPage from "@pages/video/no-meeting-found";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import NoMeetingFound from "~/videos/views/videos-no-meeting-found-single-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("no_meeting_found"),
|
||||
@@ -20,4 +21,4 @@ const getData = async (context: GetServerSidePropsContext) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
|
||||
export default WithLayout({ getData, Page: NoMeetingFound, getLayout: null })<"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">;
|
||||
|
||||
@@ -16,10 +16,10 @@ import { Plus } from "@calcom/ui/components/icon";
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
interface AdditionalCalendarSelectorProps {
|
||||
isLoading?: boolean;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
|
||||
@@ -42,7 +42,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import type { IframeHTMLAttributes } from "react";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
||||
@@ -92,17 +92,19 @@ export const AppPage = ({
|
||||
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
|
||||
|
||||
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
|
||||
{ appType: type },
|
||||
{
|
||||
onSettled(data) {
|
||||
const credentialsCount = data?.credentials.length || 0;
|
||||
setShowDisconnectIntegration(
|
||||
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
|
||||
);
|
||||
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
|
||||
},
|
||||
}
|
||||
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
const data = appDbQuery.data;
|
||||
|
||||
const credentialsCount = data?.credentials.length || 0;
|
||||
setShowDisconnectIntegration(
|
||||
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
|
||||
);
|
||||
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
|
||||
},
|
||||
[appDbQuery.data]
|
||||
);
|
||||
|
||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||
@@ -190,7 +192,7 @@ export const AppPage = ({
|
||||
)}
|
||||
</header>
|
||||
</div>
|
||||
{!appDbQuery.isLoading ? (
|
||||
{!appDbQuery.isPending ? (
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
@@ -211,7 +213,7 @@ export const AppPage = ({
|
||||
onClick: () => {
|
||||
mutation.mutate({ type, variant, slug });
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
loading: mutation.isPending,
|
||||
};
|
||||
}
|
||||
return (
|
||||
@@ -250,7 +252,7 @@ export const AppPage = ({
|
||||
onClick: () => {
|
||||
mutation.mutate({ type, variant, slug });
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
loading: mutation.isPending,
|
||||
};
|
||||
}
|
||||
return (
|
||||
@@ -272,7 +274,7 @@ export const AppPage = ({
|
||||
)}
|
||||
|
||||
{dependencies &&
|
||||
(!dependencyData.isLoading ? (
|
||||
(!dependencyData.isPending ? (
|
||||
<div className="mt-6">
|
||||
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
@@ -28,7 +30,7 @@ type Props = {
|
||||
onChanged: () => unknown | Promise<unknown>;
|
||||
fromOnboarding?: boolean;
|
||||
destinationCalendarId?: string;
|
||||
isLoading?: boolean;
|
||||
isPending?: boolean;
|
||||
};
|
||||
|
||||
function CalendarList(props: Props) {
|
||||
@@ -74,7 +76,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
suspense: true,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { fromOnboarding, isLoading } = props;
|
||||
const { fromOnboarding, isPending } = props;
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
@@ -97,7 +99,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||
{!!data.connectedCalendars.length && (
|
||||
<div className="flex items-center">
|
||||
<AdditionalCalendarSelector isLoading={isLoading} />
|
||||
<AdditionalCalendarSelector isPending={isPending} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -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([
|
||||
@@ -229,7 +240,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
hidePlaceholder
|
||||
value={data.destinationCalendar?.externalId}
|
||||
onChange={mutation.mutate}
|
||||
isLoading={mutation.isLoading}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,7 +250,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isLoading={mutation.isLoading}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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";
|
||||
@@ -100,9 +100,9 @@ export const InstallAppButtonChild = ({
|
||||
<DropdownMenuContent
|
||||
className="w-auto"
|
||||
onInteractOutside={(event) => {
|
||||
if (mutation.isLoading) event.preventDefault();
|
||||
if (mutation.isPending) event.preventDefault();
|
||||
}}>
|
||||
{mutation.isLoading && (
|
||||
{mutation.isPending && (
|
||||
<div className="z-1 fixed inset-0 flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -139,7 +139,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: Ban,
|
||||
disabled: mutation.isLoading,
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
...((isPending && !paymentAppData.enabled) ||
|
||||
@@ -153,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: Check,
|
||||
disabled: mutation.isLoading,
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -293,7 +293,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
color: showCheckRecordingButton ? "secondary" : "primary",
|
||||
disabled: mutation.isLoading,
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
disabled={mutation.isLoading}
|
||||
disabled={mutation.isPending}
|
||||
data-testid="rejection-confirm"
|
||||
onClick={() => {
|
||||
bookingConfirm(false);
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="send_request"
|
||||
disabled={chargeCardMutation.isLoading || chargeError}
|
||||
disabled={chargeCardMutation.isPending || chargeError}
|
||||
onClick={() =>
|
||||
chargeCardMutation.mutate({
|
||||
bookingId,
|
||||
|
||||
@@ -27,7 +27,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||
|
||||
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
|
||||
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("reschedule_request_sent"), "success");
|
||||
setIsOpenDialog(false);
|
||||
@@ -65,7 +65,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="send_request"
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
rescheduleApi({
|
||||
bookingId,
|
||||
|
||||
@@ -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,7 +19,8 @@ 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 { 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";
|
||||
@@ -31,32 +31,27 @@ import {
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
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(!!eventType.hashedLink);
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||
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,28 +60,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = `${name} input`;
|
||||
});
|
||||
|
||||
const eventNameObject: EventNameObjectType = {
|
||||
attendeeName: t("scheduler"),
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: eventType.users[0]?.name || "Nameless",
|
||||
eventType: formMethods.getValues("title"),
|
||||
eventName: formMethods.getValues("eventName"),
|
||||
host: formMethods.getValues("users")[0]?.name || "Nameless",
|
||||
bookingFields: bookingFields,
|
||||
t,
|
||||
};
|
||||
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
||||
formMethods.getValues("requiresConfirmation")
|
||||
);
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
|
||||
const noShowFeeEnabled =
|
||||
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
||||
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
}, [eventType.users, hashedUrl, team?.id]);
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
|
||||
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
|
||||
|
||||
const toggleGuests = (enabled: boolean) => {
|
||||
const bookingFields = formMethods.getValues("bookingFields");
|
||||
@@ -103,12 +101,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
})
|
||||
}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
@@ -121,8 +120,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
|
||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/**
|
||||
@@ -131,7 +131,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>
|
||||
@@ -145,12 +145,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</Link>
|
||||
</div>
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="destinationCalendar"
|
||||
defaultValue={eventType.destinationCalendar || undefined}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<DestinationCalendarSelector
|
||||
destinationCalendar={eventType.destinationCalendar}
|
||||
value={value ? value.externalId : undefined}
|
||||
onChange={onChange}
|
||||
hidePlaceholder
|
||||
@@ -167,7 +164,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
type="text"
|
||||
{...shouldLockDisableProps("eventName")}
|
||||
placeholder={eventNamePlaceholder}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventName")}
|
||||
addOnSuffix={
|
||||
<Button
|
||||
@@ -181,6 +177,27 @@ 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
@@ -194,7 +211,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{...shouldLockDisableProps("bookingFields")}
|
||||
dataStore={{
|
||||
options: {
|
||||
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
||||
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -203,21 +220,20 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<RequiresConfirmationController
|
||||
eventType={eventType}
|
||||
seatsEnabled={seatsEnabled}
|
||||
metadata={eventType.metadata}
|
||||
metadata={formMethods.getValues("metadata")}
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
onRequiresConfirmation={setRequiresConfirmation}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="requiresBookerEmailVerification"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.requiresBookerEmailVerification}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("requires_booker_email_verification")}
|
||||
data-testid="requires-booker-email-verification"
|
||||
{...shouldLockDisableProps("requiresBookerEmailVerification")}
|
||||
description={t("description_requires_booker_email_verification")}
|
||||
checked={value}
|
||||
@@ -228,14 +244,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
<Controller
|
||||
name="hideCalendarNotes"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.hideCalendarNotes}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_notes")}
|
||||
data-testid="disable-notes"
|
||||
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||
description={t("disable_notes_description")}
|
||||
checked={value}
|
||||
@@ -246,7 +261,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
<Controller
|
||||
name="successRedirectUrl"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
@@ -258,6 +272,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
)}
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("redirect_success_booking")}
|
||||
data-testid="redirect-success-booking"
|
||||
{...successRedirectUrlLocked}
|
||||
description={t("redirect_url_description")}
|
||||
checked={redirectUrlVisible}
|
||||
@@ -272,16 +287,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
labelSrOnly
|
||||
disabled={successRedirectUrlLocked.disabled}
|
||||
placeholder={t("external_redirect_url")}
|
||||
data-testid="external-redirect-url"
|
||||
required={redirectUrlVisible}
|
||||
type="text"
|
||||
defaultValue={eventType.successRedirectUrl || ""}
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
"p-1 text-sm text-orange-600",
|
||||
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||
)}>
|
||||
)}
|
||||
data-testid="redirect-url-warning">
|
||||
{t("redirect_url_warning")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,6 +318,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
title={t("enable_private_url")}
|
||||
Badge={
|
||||
<a
|
||||
data-testid="hashedLinkCheck-info"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://cal.com/docs/core-features/event-types/single-use-private-links">
|
||||
@@ -312,7 +329,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">
|
||||
@@ -327,7 +344,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Tooltip
|
||||
content={
|
||||
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||
}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
@@ -336,7 +356,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
if (formMethods.getValues("hashedLink")) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
@@ -353,8 +373,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
<Controller
|
||||
name="seatsPerTimeSlotEnabled"
|
||||
control={formMethods.control}
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
@@ -375,10 +393,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", 2);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
@@ -388,8 +408,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div>
|
||||
<TextField
|
||||
@@ -399,30 +417,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
disabled={seatsLocked.disabled}
|
||||
defaultValue={value || 2}
|
||||
defaultValue={value}
|
||||
min={1}
|
||||
containerClassName="max-w-80"
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
data-testid="seats-per-time-slot"
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<CheckboxField
|
||||
description={t("show_attendees")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
<Controller
|
||||
name="seatsShowAttendees"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CheckboxField
|
||||
data-testid="show-attendees"
|
||||
description={t("show_attendees")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => onChange(e)}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CheckboxField
|
||||
description={t("show_available_seats_count")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) =>
|
||||
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
|
||||
}
|
||||
defaultChecked={!!eventType.seatsShowAvailabilityCount}
|
||||
<Controller
|
||||
name="seatsShowAvailabilityCount"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CheckboxField
|
||||
description={t("show_available_seats_count")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => onChange(e)}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -436,8 +464,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
/>
|
||||
<Controller
|
||||
name="lockTimeZoneToggleOnBookingPage"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
@@ -448,63 +474,52 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("description_lock_timezone_toggle_on_booking_page")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
data-testid="lock-timezone-toggle"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{allowDisablingAttendeeConfirmationEmails(workflows) && (
|
||||
<>
|
||||
<Controller
|
||||
name="metadata.disableStandardEmails.confirmation.attendee"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_attendees_confirmation_emails")}
|
||||
description={t("disable_attendees_confirmation_emails_description")}
|
||||
checked={value || false}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
|
||||
onChange(e);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<Controller
|
||||
name="metadata.disableStandardEmails.confirmation.attendee"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_attendees_confirmation_emails")}
|
||||
description={t("disable_attendees_confirmation_emails_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{allowDisablingHostConfirmationEmails(workflows) && (
|
||||
<>
|
||||
<Controller
|
||||
name="metadata.disableStandardEmails.confirmation.host"
|
||||
control={formMethods.control}
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_host_confirmation_emails")}
|
||||
description={t("disable_host_confirmation_emails_description")}
|
||||
checked={value || false}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
|
||||
onChange(e);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<Controller
|
||||
name="metadata.disableStandardEmails.confirmation.host"
|
||||
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_host_confirmation_emails")}
|
||||
description={t("disable_host_confirmation_emails_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showEventNameTip && (
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={setEventName}
|
||||
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
|
||||
defaultValue={formMethods.getValues("eventName")}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
/>
|
||||
|
||||
@@ -18,23 +18,27 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
|
||||
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const { t } = useLocale();
|
||||
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||
extendsFeature: "EventType",
|
||||
teamId: eventType.team?.id || eventType.parent?.teamId,
|
||||
});
|
||||
|
||||
const methods = useFormContext<FormValues>();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const installedApps =
|
||||
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
|
||||
const notInstalledApps =
|
||||
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
|
||||
const allAppsData = methods.watch("metadata")?.apps || {};
|
||||
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
methods.setValue("metadata", {
|
||||
...methods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata",
|
||||
{
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
@@ -47,7 +51,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const eventTypeFormMetadata = methods.getValues("metadata");
|
||||
const eventTypeFormMetadata = formMethods.getValues("metadata");
|
||||
|
||||
const getAppDataSetter = (
|
||||
appId: EventTypeAppsList,
|
||||
@@ -56,8 +60,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
): SetAppData => {
|
||||
return function (key, value) {
|
||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||
|
||||
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
|
||||
const appData = allAppsDataFromForm[appId];
|
||||
setAllAppsData({
|
||||
...allAppsDataFromForm,
|
||||
@@ -138,7 +141,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
message={t("locked_apps_description")}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !installedApps?.length ? (
|
||||
{!isPending && !installedApps?.length ? (
|
||||
<EmptyScreen
|
||||
Icon={Grid}
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
@@ -179,7 +182,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
</div>
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
<div className="bg-muted mt-6 rounded-md p-8">
|
||||
{!isLoading && notInstalledApps?.length ? (
|
||||
{!isPending && notInstalledApps?.length ? (
|
||||
<>
|
||||
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
|
||||
{t("available_apps_lower_case")}
|
||||
|
||||
@@ -85,7 +85,7 @@ const EventTypeScheduleDetails = memo(
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
const { isPending, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{
|
||||
scheduleId:
|
||||
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
||||
@@ -112,7 +112,7 @@ const EventTypeScheduleDetails = memo(
|
||||
)}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
{isPending ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3 text-right">
|
||||
@@ -142,7 +142,7 @@ const EventTypeScheduleDetails = memo(
|
||||
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
|
||||
<Button
|
||||
href={`/availability/${schedule.id}`}
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
color="minimal"
|
||||
EndIcon={ExternalLink}
|
||||
target="_blank"
|
||||
@@ -169,8 +169,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const watchSchedule = watch("schedule");
|
||||
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||
|
||||
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
|
||||
onSuccess: ({ schedules }) => {
|
||||
const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const schedules = data.schedules;
|
||||
|
||||
const options = schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
@@ -223,15 +230,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
setValue("availability", value);
|
||||
setValue("availability", value, { shouldDirty: true });
|
||||
},
|
||||
});
|
||||
|
||||
[data]
|
||||
);
|
||||
const availabilityValue = watch("availability");
|
||||
|
||||
useEffect(() => {
|
||||
if (!availabilityValue?.value) return;
|
||||
setValue("schedule", availabilityValue.value);
|
||||
setValue("schedule", availabilityValue.value, { shouldDirty: true });
|
||||
}, [availabilityValue, setValue]);
|
||||
|
||||
return (
|
||||
@@ -241,8 +248,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
{t("availability")}
|
||||
{shouldLockIndicator("availability")}
|
||||
</label>
|
||||
{isLoading && <SelectSkeletonLoader />}
|
||||
{!isLoading && (
|
||||
{isPending && <SelectSkeletonLoader />}
|
||||
{!isPending && (
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => {
|
||||
@@ -253,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}
|
||||
@@ -291,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")}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { FormValues } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
@@ -107,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
export const EventLimitsTab = () => {
|
||||
const { t, i18n } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
@@ -126,47 +127,29 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
},
|
||||
];
|
||||
|
||||
const periodType =
|
||||
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
||||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
|
||||
|
||||
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
||||
startDate: new Date(eventType.periodStartDate || Date.now()),
|
||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||
});
|
||||
const watchPeriodType = useWatch({
|
||||
control: formMethods.control,
|
||||
name: "periodType",
|
||||
defaultValue: periodType?.type,
|
||||
});
|
||||
const watchPeriodType = formMethods.watch("periodType");
|
||||
|
||||
const optionsPeriod = [
|
||||
{ value: 1, label: t("calendar_days") },
|
||||
{ value: 0, label: t("business_days") },
|
||||
{ value: 1, label: t("calendar_days") },
|
||||
];
|
||||
|
||||
// offsetStart toggle is client-side only, opened by default if offsetStart is set
|
||||
const offsetStartValue = useWatch({
|
||||
control: formMethods.control,
|
||||
name: "offsetStart",
|
||||
});
|
||||
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
|
||||
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
|
||||
|
||||
// Preview how the offset will affect start times
|
||||
const watchOffsetStartValue = formMethods.watch("offsetStart");
|
||||
const offsetOriginalTime = new Date();
|
||||
offsetOriginalTime.setHours(9, 0, 0, 0);
|
||||
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
|
||||
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
|
||||
<Label htmlFor="beforeEventBuffer">{t("before_event")}</Label>
|
||||
<Controller
|
||||
name="beforeBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.beforeEventBuffer || 0}
|
||||
name="beforeEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const beforeBufferOptions = [
|
||||
{
|
||||
@@ -194,11 +177,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
|
||||
<Label htmlFor="afterEventBuffer">{t("after_event")}</Label>
|
||||
<Controller
|
||||
name="afterBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.afterEventBuffer || 0}
|
||||
name="afterEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const afterBufferOptions = [
|
||||
{
|
||||
@@ -235,7 +216,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const slotIntervalOptions = [
|
||||
{
|
||||
@@ -251,11 +231,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<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((option) => option.value === eventType.slotInterval) ||
|
||||
slotIntervalOptions[0]
|
||||
slotIntervalOptions.find(
|
||||
(option) => option.value === formMethods.getValues("slotInterval")
|
||||
) || slotIntervalOptions[0]
|
||||
}
|
||||
options={slotIntervalOptions}
|
||||
/>
|
||||
@@ -267,7 +250,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
</div>
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
@@ -279,11 +261,15 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
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(
|
||||
@@ -300,8 +286,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -311,7 +296,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
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",
|
||||
@@ -323,8 +308,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -340,11 +324,11 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
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">
|
||||
@@ -361,8 +345,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
<Controller
|
||||
name="periodType"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value && value !== "UNLIMITED";
|
||||
|
||||
return (
|
||||
@@ -377,12 +360,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
onCheckedChange={(bool) =>
|
||||
// formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED", { shouldDirty: true })
|
||||
onChange(bool ? "ROLLING" : "UNLIMITED")
|
||||
}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<RadioGroup.Root
|
||||
defaultValue={watchPeriodType}
|
||||
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 (
|
||||
@@ -408,22 +395,27 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
placeholder="30"
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
/>
|
||||
<Select
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) => {
|
||||
onChange={(opt) =>
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value.toString() as "0" | "1"
|
||||
);
|
||||
}}
|
||||
defaultValue={
|
||||
optionsPeriod.find(
|
||||
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
||||
) ?? optionsPeriod[0]
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
|
||||
})}
|
||||
defaultValue={optionsPeriod.find(
|
||||
(opt) =>
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -431,14 +423,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
control={formMethods.control}
|
||||
defaultValue={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,
|
||||
});
|
||||
@@ -472,7 +462,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
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">
|
||||
@@ -481,7 +471,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
type="number"
|
||||
containerClassName="max-w-80"
|
||||
label={t("offset_start")}
|
||||
{...formMethods.register("offsetStart")}
|
||||
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
hint={t("offset_start_description", {
|
||||
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||
@@ -603,11 +593,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 (
|
||||
@@ -637,7 +632,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;
|
||||
|
||||
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
||||
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
||||
</div>
|
||||
);
|
||||
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -48,21 +48,19 @@ const getLocationFromType = (
|
||||
}
|
||||
};
|
||||
|
||||
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||
const getLocationInfo = ({
|
||||
eventType,
|
||||
locationOptions,
|
||||
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||
const locationAvailable =
|
||||
props.eventType.locations &&
|
||||
props.eventType.locations.length > 0 &&
|
||||
props.locationOptions.some((op) =>
|
||||
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
|
||||
);
|
||||
const locationDetails = props.eventType.locations &&
|
||||
props.eventType.locations.length > 0 &&
|
||||
eventType.locations &&
|
||||
eventType.locations.length > 0 &&
|
||||
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
|
||||
const locationDetails = eventType.locations &&
|
||||
eventType.locations.length > 0 &&
|
||||
!locationAvailable && {
|
||||
slug: props.eventType.locations[0].type
|
||||
.replace("integrations:", "")
|
||||
.replace(":", "-")
|
||||
.replace("_video", ""),
|
||||
name: props.eventType.locations[0].type
|
||||
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
|
||||
name: eventType.locations[0].type
|
||||
.replace("integrations:", "")
|
||||
.replace(":", " ")
|
||||
.replace("_video", "")
|
||||
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
|
||||
};
|
||||
return { locationAvailable, locationDetails };
|
||||
};
|
||||
interface DescriptionEditorProps {
|
||||
description?: string | null;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const [mounted, setIsMounted] = useState(false);
|
||||
const { t } = useLocale();
|
||||
const { description } = props;
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||
|
||||
return mounted ? (
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={props.editable}
|
||||
editable={isEditable}
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
/>
|
||||
@@ -113,7 +106,9 @@ export const EventSetupTab = (
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { eventType, team, destinationCalendar } = props;
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||
const [multipleDuration, setMultipleDuration] = useState(
|
||||
formMethods.getValues("metadata")?.multipleDuration
|
||||
);
|
||||
const orgBranding = useOrgBranding();
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
|
||||
@@ -143,12 +138,12 @@ export const EventSetupTab = (
|
||||
}>
|
||||
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
|
||||
const [defaultDuration, setDefaultDuration] = useState(
|
||||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||
selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
|
||||
);
|
||||
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||
useLockedFieldsManager(
|
||||
eventType,
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
@@ -195,7 +190,6 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
control={formMethods.control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
@@ -219,7 +213,6 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<Controller
|
||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||
control={formMethods.control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
@@ -339,7 +332,7 @@ export const EventSetupTab = (
|
||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) => {
|
||||
const fieldValues = formMethods.getValues().locations[index];
|
||||
const fieldValues = formMethods.getValues("locations")[index];
|
||||
updateLocationField(index, {
|
||||
...fieldValues,
|
||||
displayLocationPublicly: e.target.checked,
|
||||
@@ -408,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/${eventType.id}?tabName=advanced`}
|
||||
href={`${WEBAPP_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||
className="underline">
|
||||
here.
|
||||
</Link>{" "}
|
||||
@@ -419,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>
|
||||
@@ -451,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>
|
||||
@@ -463,7 +457,6 @@ export const EventSetupTab = (
|
||||
required
|
||||
label={t("title")}
|
||||
{...shouldLockDisableProps("title")}
|
||||
defaultValue={eventType.title}
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
@@ -471,23 +464,19 @@ export const EventSetupTab = (
|
||||
{t("description")}
|
||||
{shouldLockIndicator("description")}
|
||||
</Label>
|
||||
<DescriptionEditor
|
||||
description={eventType?.description}
|
||||
editable={!descriptionLockedProps.disabled}
|
||||
/>
|
||||
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
label={t("URL")}
|
||||
{...shouldLockDisableProps("slug")}
|
||||
defaultValue={eventType.slug}
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/
|
||||
{!isManagedEventType
|
||||
? team
|
||||
? (orgBranding ? "" : "team/") + team.slug
|
||||
: eventType.users[0].username
|
||||
: formMethods.getValues("users")[0].username
|
||||
: t("username_placeholder")}
|
||||
/
|
||||
</>
|
||||
@@ -523,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>
|
||||
@@ -553,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>
|
||||
@@ -564,7 +553,7 @@ export const EventSetupTab = (
|
||||
type="number"
|
||||
{...lengthLockedProps}
|
||||
label={t("duration")}
|
||||
defaultValue={eventType.length ?? 15}
|
||||
defaultValue={formMethods.getValues("length") ?? 15}
|
||||
{...formMethods.register("length")}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
min={1}
|
||||
@@ -580,12 +569,14 @@ export const EventSetupTab = (
|
||||
onCheckedChange={() => {
|
||||
if (multipleDuration !== undefined) {
|
||||
setMultipleDuration(undefined);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
setSelectedMultipleDuration([]);
|
||||
setDefaultDuration(null);
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -600,12 +591,7 @@ export const EventSetupTab = (
|
||||
{shouldLockIndicator("locations")}
|
||||
</Skeleton>
|
||||
|
||||
<Controller
|
||||
name="locations"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.locations || []}
|
||||
render={() => <Locations />}
|
||||
/>
|
||||
<Controller name="locations" render={() => <Locations />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
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";
|
||||
import type { Options } from "react-select";
|
||||
|
||||
@@ -11,7 +11,7 @@ import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTea
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
import { Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
interface IUserToValue {
|
||||
id: number | null;
|
||||
@@ -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})` : ""}`,
|
||||
@@ -97,6 +104,43 @@ const ChildrenEventTypesList = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AssignAllTeamMembers = ({
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
onActive,
|
||||
onInactive,
|
||||
}: {
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
onActive: () => void;
|
||||
onInactive?: () => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
name="assignAllTeamMembers"
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("automatically_add_all_team_members")}
|
||||
labelClassName="mt-0.5 font-normal"
|
||||
checked={assignAllTeamMembers}
|
||||
onCheckedChange={(active) => {
|
||||
setValue("assignAllTeamMembers", active, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(active);
|
||||
if (active) {
|
||||
onActive();
|
||||
} else if (!!onInactive) {
|
||||
onInactive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckedHostField = ({
|
||||
labelText,
|
||||
placeholder,
|
||||
@@ -107,18 +151,18 @@ const CheckedHostField = ({
|
||||
helperText,
|
||||
...rest
|
||||
}: {
|
||||
labelText: string;
|
||||
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>
|
||||
<Label>{labelText}</Label>
|
||||
{labelText ? <Label>{labelText}</Label> : <></>}
|
||||
<CheckedTeamSelect
|
||||
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
|
||||
onChange={(options) => {
|
||||
@@ -127,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>
|
||||
);
|
||||
@@ -161,76 +204,255 @@ 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,
|
||||
onChange,
|
||||
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 { setValue } = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
isFixed={true}
|
||||
onChange={(changeValue) => {
|
||||
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
|
||||
}}
|
||||
value={value}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
helperText={FixedHostHelper}
|
||||
/>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
|
||||
value={value}
|
||||
isFixed={false}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("round_robin_hosts")}
|
||||
helperText={t("round_robin_helper")}
|
||||
/>
|
||||
</>
|
||||
<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}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChildrenEventTypes = ({
|
||||
childrenEventTypeOptions,
|
||||
assignAllTeamMembers,
|
||||
setAssignAllTeamMembers,
|
||||
}: {
|
||||
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||
assignAllTeamMembers: boolean;
|
||||
setAssignAllTeamMembers: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { setValue } = useFormContext<FormValues>();
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
name="children"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<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={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
|
||||
/>
|
||||
{!assignAllTeamMembers ? (
|
||||
<Controller<FormValues>
|
||||
name="children"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Hosts = ({
|
||||
teamMembers,
|
||||
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>();
|
||||
@@ -250,10 +472,12 @@ 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]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
@@ -261,26 +485,39 @@ const Hosts = ({
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const schedulingTypeRender = {
|
||||
COLLECTIVE: (
|
||||
<CheckedHostField
|
||||
<FixedHosts
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isFixed={true}
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
/>
|
||||
),
|
||||
ROUND_ROBIN: (
|
||||
<>
|
||||
<RoundRobinHosts 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")}</>}
|
||||
/>*/}
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
MANAGED: <></>,
|
||||
@@ -320,34 +557,61 @@ export const EventTeamTab = ({
|
||||
.filter(pendingMembers)
|
||||
.map((member) => mapUserToValue(member, t("pending")));
|
||||
const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
|
||||
return mapMemberToChildrenOption(member, eventType.slug, t("pending"));
|
||||
return mapMemberToChildrenOption(
|
||||
{ ...member, eventTypes: member.eventTypes.filter((et) => et !== eventType.slug) },
|
||||
eventType.slug,
|
||||
t("pending")
|
||||
);
|
||||
});
|
||||
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||
const { getValues, setValue } = useFormContext<FormValues>();
|
||||
const [assignAllTeamMembers, setAssignAllTeamMembers] = useState<boolean>(
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<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 teamMembers={teamMembersOptions} />
|
||||
</div>
|
||||
<Hosts
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
teamMembers={teamMembersOptions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{team && isManagedEventType && (
|
||||
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
|
||||
<ChildrenEventTypes
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
childrenEventTypeOptions={childrenEventTypeOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
@@ -67,61 +68,126 @@ type Props = {
|
||||
availability?: AvailabilityOption;
|
||||
isUserOrganizationAdmin: boolean;
|
||||
bookerUrl: string;
|
||||
activeWebhooksNumber: number;
|
||||
};
|
||||
|
||||
function getNavigation(props: {
|
||||
type getNavigationProps = {
|
||||
t: TFunction;
|
||||
eventType: Props["eventType"];
|
||||
length: number;
|
||||
id: number;
|
||||
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
|
||||
enabledAppsNumber: number;
|
||||
enabledWorkflowsNumber: number;
|
||||
installedAppsNumber: number;
|
||||
availability: AvailabilityOption | undefined;
|
||||
}) {
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||
const duration =
|
||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||
};
|
||||
|
||||
function getNavigation({
|
||||
length,
|
||||
id,
|
||||
multipleDuration,
|
||||
t,
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
enabledWorkflowsNumber,
|
||||
}: getNavigationProps) {
|
||||
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "event_setup_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||
href: `/event-types/${id}?tabName=setup`,
|
||||
icon: LinkIcon,
|
||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "event_limit_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||
href: `/event-types/${id}?tabName=limits`,
|
||||
icon: Clock,
|
||||
info: `event_limit_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "event_advanced_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||
href: `/event-types/${id}?tabName=advanced`,
|
||||
icon: Sliders,
|
||||
info: `event_advanced_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||
href: `/event-types/${id}?tabName=recurring`,
|
||||
icon: Repeat,
|
||||
info: `recurring_event_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||
href: `/event-types/${id}?tabName=apps`,
|
||||
icon: Grid,
|
||||
//TODO: Handle proper translation with count handling
|
||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||
href: `/event-types/${id}?tabName=workflows`,
|
||||
icon: Zap,
|
||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -136,47 +202,35 @@ function EventTypeSingleLayout({
|
||||
availability,
|
||||
isUserOrganizationAdmin,
|
||||
bookerUrl,
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
currentUserMembership?.role !== "MEMBER" ||
|
||||
!currentUserMembership ||
|
||||
eventType.schedulingType === SchedulingType.MANAGED ||
|
||||
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(
|
||||
eventType,
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
const length = formMethods.watch("length");
|
||||
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
|
||||
|
||||
const watchSchedulingType = formMethods.watch("schedulingType");
|
||||
const watchChildrenCount = formMethods.watch("children").length;
|
||||
// Define tab navigation here
|
||||
const EventTypeTabs = useMemo(() => {
|
||||
const navigation = getNavigation({
|
||||
t,
|
||||
eventType,
|
||||
length,
|
||||
multipleDuration,
|
||||
id: formMethods.getValues("id"),
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
enabledWorkflowsNumber,
|
||||
@@ -185,31 +239,29 @@ function EventTypeSingleLayout({
|
||||
|
||||
navigation.splice(1, 0, {
|
||||
name: "availability",
|
||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
|
||||
icon: Calendar,
|
||||
info:
|
||||
isManagedEventType || isChildrenManagedEventType
|
||||
? eventType.schedule === null
|
||||
? formMethods.getValues("schedule") === null
|
||||
? "members_default_schedule"
|
||||
: isChildrenManagedEventType
|
||||
? `${
|
||||
eventType.scheduleName
|
||||
? `${eventType.scheduleName} - ${t("managed")}`
|
||||
formMethods.getValues("scheduleName")
|
||||
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
|
||||
: `default_schedule_name`
|
||||
}`
|
||||
: eventType.scheduleName ?? `default_schedule_name`
|
||||
: eventType.scheduleName ?? `default_schedule_name`,
|
||||
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
|
||||
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
|
||||
});
|
||||
// If there is a team put this navigation item within the tabs
|
||||
if (team) {
|
||||
navigation.splice(2, 0, {
|
||||
name: "assignment",
|
||||
href: `/event-types/${eventType.id}?tabName=team`,
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
|
||||
icon: Users,
|
||||
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
||||
isManagedEventType
|
||||
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
|
||||
: ""
|
||||
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
|
||||
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
|
||||
}`,
|
||||
});
|
||||
}
|
||||
@@ -225,15 +277,14 @@ function EventTypeSingleLayout({
|
||||
}
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
|
||||
icon: TbWebhook,
|
||||
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
return navigation;
|
||||
}, [
|
||||
t,
|
||||
eventType,
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
enabledWorkflowsNumber,
|
||||
@@ -241,16 +292,23 @@ function EventTypeSingleLayout({
|
||||
isManagedEventType,
|
||||
isChildrenManagedEventType,
|
||||
team,
|
||||
formMethods,
|
||||
length,
|
||||
multipleDuration,
|
||||
formMethods.getValues("id"),
|
||||
watchSchedulingType,
|
||||
watchChildrenCount,
|
||||
activeWebhooksNumber,
|
||||
]);
|
||||
|
||||
const permalink = `${bookerUrl}/${
|
||||
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
|
||||
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
|
||||
}/${eventType.slug}`;
|
||||
|
||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
||||
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||
|
||||
const embedLink = `${
|
||||
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
|
||||
}/${formMethods.getValues("slug")}`;
|
||||
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
|
||||
// const title = formMethods.watch("title");
|
||||
return (
|
||||
<Shell
|
||||
backPath="/event-types"
|
||||
@@ -258,7 +316,7 @@ function EventTypeSingleLayout({
|
||||
heading={eventType.title}
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
{!eventType.metadata?.managedEventConfig && (
|
||||
{!formMethods.getValues("metadata")?.managedEventConfig && (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -285,7 +343,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -329,11 +387,11 @@ function EventTypeSingleLayout({
|
||||
StartIcon={Code}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
namespace={eventType.slug}
|
||||
namespace=""
|
||||
tooltip={t("embed")}
|
||||
tooltipSide="bottom"
|
||||
tooltipOffset={4}
|
||||
eventId={eventType.id}
|
||||
eventId={formMethods.getValues("id")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -401,7 +459,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -412,6 +470,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")}
|
||||
@@ -444,31 +503,13 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
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: eventType.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>
|
||||
);
|
||||
|
||||
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess() {
|
||||
setEditModalOpen(false);
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
showToast(t("webhook_updated_successfully"), "success");
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
|
||||
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||
async onSuccess() {
|
||||
setCreateModalOpen(false);
|
||||
showToast(t("webhook_created_successfully"), "success");
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
setCreateModalOpen(false);
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}>
|
||||
@@ -108,7 +108,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
eventTypeId: eventType.id,
|
||||
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
||||
});
|
||||
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||
variant: "other",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
@@ -188,7 +188,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{webhooks && !isLoading && (
|
||||
{webhooks && !isPending && (
|
||||
<>
|
||||
<div>
|
||||
{webhooks.length ? (
|
||||
|
||||
@@ -8,18 +8,18 @@ interface ManagedEventDialogProps {
|
||||
slugExistsChildrenDialogOpen: ChildrenEventType[];
|
||||
slug: string;
|
||||
onOpenChange: () => void;
|
||||
isLoading: boolean;
|
||||
isPending: boolean;
|
||||
onConfirm: (e: { preventDefault: () => void }) => void;
|
||||
}
|
||||
|
||||
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
|
||||
const { t } = useLocale();
|
||||
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
|
||||
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={isLoading}
|
||||
isPending={isPending}
|
||||
variety="warning"
|
||||
title={t("managed_event_dialog_title", {
|
||||
slug,
|
||||
|
||||
@@ -19,11 +19,10 @@ export default function RecurringEventController({
|
||||
paymentEnabled,
|
||||
}: RecurringEventControllerProps) {
|
||||
const { t } = useLocale();
|
||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||
eventType.recurringEvent
|
||||
);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||
formMethods.getValues("recurringEvent")
|
||||
);
|
||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||
const recurringEventFreqOptions = Object.entries(Frequency)
|
||||
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
|
||||
@@ -67,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 || {
|
||||
@@ -75,7 +74,7 @@ export default function RecurringEventController({
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
@@ -96,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);
|
||||
}}
|
||||
/>
|
||||
@@ -111,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);
|
||||
}}
|
||||
/>
|
||||
@@ -130,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]);
|
||||
@@ -75,13 +75,14 @@ export default function RequiresConfirmationController({
|
||||
)}
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("requires_confirmation")}
|
||||
data-testid="requires-confirmation"
|
||||
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
|
||||
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
||||
description={t("requires_confirmation_description")}
|
||||
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">
|
||||
@@ -95,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 }
|
||||
);
|
||||
}
|
||||
}}>
|
||||
@@ -145,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"
|
||||
@@ -170,7 +175,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
opt?.value as UnitTypeLongPlural,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -28,7 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
color="secondary"
|
||||
disabled={installed}
|
||||
type="button"
|
||||
loading={buttonProps?.isLoading}
|
||||
loading={buttonProps?.isPending}
|
||||
onClick={(event) => {
|
||||
// Save cookie key to return url step
|
||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||
|
||||
@@ -31,7 +31,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
return (
|
||||
<>
|
||||
{/* Already connected calendars */}
|
||||
{!queryConnectedCalendars.isLoading &&
|
||||
{!queryConnectedCalendars.isPending &&
|
||||
firstCalendar &&
|
||||
firstCalendar.integration &&
|
||||
firstCalendar.integration.title &&
|
||||
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
</List>
|
||||
)}
|
||||
|
||||
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
||||
{queryIntegrations.isPending && <StepConnectionLoader />}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,7 +13,7 @@ interface ConnectedAppStepProps {
|
||||
|
||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
const { nextStep } = props;
|
||||
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||
variant: "conferencing",
|
||||
onlyInstalled: false,
|
||||
sortByMostPopular: true,
|
||||
@@ -26,7 +26,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && (
|
||||
{!isPending && (
|
||||
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||
{queryConnectedVideoApps?.items &&
|
||||
queryConnectedVideoApps?.items.map((item) => {
|
||||
@@ -48,7 +48,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
</List>
|
||||
)}
|
||||
|
||||
{isLoading && <StepConnectionLoader />}
|
||||
{isPending && <StepConnectionLoader />}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="save-video-button"
|
||||
|
||||
@@ -8,7 +8,6 @@ import { md } from "@calcom/lib/markdownIt";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { UserAvatar } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
@@ -97,18 +96,10 @@ const UserProfile = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const organization =
|
||||
user.organization && user.organization.id
|
||||
? {
|
||||
...(user.organization as Ensure<typeof user.organization, "id">),
|
||||
slug: user.organization.slug || null,
|
||||
requestedSlug: user.organization.metadata?.requestedSlug || null,
|
||||
}
|
||||
: null;
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />}
|
||||
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} />}
|
||||
<input
|
||||
ref={avatarRef}
|
||||
type="hidden"
|
||||
|
||||
@@ -113,7 +113,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-8 flex w-full flex-row justify-center"
|
||||
disabled={mutation.isLoading}>
|
||||
disabled={mutation.isPending}>
|
||||
{t("next_step_text")}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user