Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab5a3149c | ||
|
|
3b8f436bdf | ||
|
|
77d45a6ec2 | ||
|
|
50c74cf286 | ||
|
|
dcbafced8b | ||
|
|
4679c80ea0 | ||
|
|
291316ee01 | ||
|
|
208fc2f0e4 | ||
|
|
99e71054ff | ||
|
|
9d118d4e34 | ||
|
+6 |
d1a877e70d | ||
|
|
238b509be6 | ||
|
|
c7471b404c | ||
|
|
4db4df8ace | ||
|
|
a15b8fb92e | ||
|
|
a4bed48eaf | ||
|
|
1be6f192d4 | ||
|
|
f1c5838aef | ||
|
|
8e994dcc33 | ||
|
|
506bbe3d31 | ||
|
|
419a84c5a2 | ||
|
|
8cf0050d1f | ||
|
|
6c23813192 | ||
|
|
875f922597 | ||
|
|
c8e3f6494a | ||
|
|
333801d2ae | ||
|
|
60f7528cbc | ||
|
|
0faf294367 | ||
|
|
87c178e53b | ||
|
|
7710b1b718 | ||
|
|
d71c6ccb31 | ||
|
|
33a4117fb5 | ||
|
|
a90419c40b | ||
|
|
16adf86c8c | ||
|
|
3b838c475a | ||
|
|
cf33106d8b | ||
|
|
a63f9ddf61 | ||
|
|
1cf83068b2 | ||
|
|
23de92f818 | ||
|
|
c6fc1f756b | ||
|
|
88b7c506f1 | ||
|
|
755aae969c | ||
|
|
2d50d09ce6 | ||
|
|
a22397dbb4 | ||
|
|
d1076982a7 | ||
|
|
d77e4f3c4a | ||
|
|
b1c0ea4c38 | ||
|
|
425e18f53c | ||
|
|
3611807838 | ||
|
|
5b1cd9e5ce | ||
|
|
00bff598ef | ||
|
|
3a8aa26c78 | ||
|
|
3c761f4451 | ||
|
|
8640eb9f52 | ||
|
|
abb5fd36f3 | ||
|
|
865663646a | ||
|
|
b7c92063c2 | ||
|
|
4f293edb6b | ||
|
|
0c88cf75e0 | ||
|
|
0b93647209 | ||
|
|
b931b00714 | ||
|
|
796a5f8d51 | ||
|
|
905a110377 | ||
|
|
6ccc19985f | ||
|
|
8f71474303 | ||
|
|
5f351230ff | ||
|
|
3e005919c1 | ||
|
|
cfa8fd8b67 | ||
|
|
a888be6e04 | ||
|
|
6aa2cba3ce | ||
|
|
12ab615684 | ||
|
|
1d792a2c6c | ||
|
|
9602ac29fb | ||
|
|
5714471b9a | ||
|
|
a99e93a4bd | ||
|
|
7f044ff92f | ||
|
|
17a0a61313 | ||
|
|
12b7739c9a | ||
|
|
ff283a25d2 | ||
|
|
4b343f20a4 | ||
|
|
5bb6904ffd | ||
|
|
0e04f5d338 | ||
|
|
da0ea5d9d0 | ||
|
|
1a19261b8f | ||
|
|
55a6c65db6 | ||
|
|
c9ed3388ae | ||
|
|
2588b7ef84 | ||
|
|
7ce83542e2 | ||
|
|
0bb0883e64 | ||
|
|
d3be62fbdd | ||
|
|
736308e43a | ||
|
|
791927e45e | ||
|
|
fa984e1568 | ||
|
|
9b197da5be | ||
|
|
b7c316b01b | ||
|
|
5ab56310bf | ||
|
|
52e318ca18 | ||
|
|
8d078564dd | ||
|
|
7b5482e83d | ||
|
|
23086476b3 | ||
|
|
00cab5c41f | ||
|
|
fbc27e1d52 | ||
|
|
e1c9a8cbfc | ||
|
|
8ee109e975 | ||
|
|
cfaecc3e02 | ||
|
|
18246e0dc1 | ||
|
|
6dcb16724c | ||
|
|
d4c5a906b5 | ||
|
|
27e8d73b82 | ||
|
|
25eae4a546 | ||
|
|
f663774fee | ||
|
|
e2258fba27 | ||
|
|
d0d8878f34 | ||
|
|
59ff6c35ee | ||
|
|
3c07e305ce | ||
|
|
b5f274d663 | ||
|
|
6c3ae984fc | ||
|
|
a2e9bf0504 | ||
|
|
8aa089efe6 | ||
|
|
de64d3d9d4 | ||
|
|
71833c0737 | ||
|
|
c78f4deaf7 | ||
|
|
24fc7d611f | ||
|
|
8afbc3b457 | ||
|
|
da18aae21c | ||
|
|
87a0d533ef | ||
|
|
7cdd453953 | ||
|
|
8143816c18 | ||
|
|
8243ac3c00 | ||
|
|
207e120d66 | ||
|
|
b25d04da1c | ||
|
|
7064acf756 | ||
|
|
716407ad90 | ||
|
|
af0b08bb2b | ||
|
|
3e27735ccb | ||
|
|
76c087f29c | ||
|
|
e05bcf98c2 | ||
|
|
636e88d467 | ||
|
|
34a61214d3 | ||
|
|
b1af30e026 | ||
|
|
870b59d617 | ||
|
|
e25094ccab | ||
|
|
1376f0991f | ||
|
|
443329b99f | ||
|
|
a1fc4ec26d | ||
|
|
d462298e79 | ||
|
|
965cff38c2 | ||
|
|
f96e909dce | ||
|
|
a4d6f7da62 | ||
|
|
ce9413e0dd | ||
|
|
8fc0dfb763 | ||
|
|
690edcf0a7 | ||
|
|
b7dbf10068 | ||
|
|
83b4ba94fc | ||
|
|
62a09e7098 | ||
|
|
d2c4e2894d | ||
|
|
070d314897 | ||
|
|
0a76368e98 | ||
|
|
99287d9eb4 | ||
|
|
100035cb89 | ||
|
|
1750dbead6 | ||
|
|
4c8ed3d11f | ||
|
|
f21c4eb11e | ||
|
|
3f71a41d7e | ||
|
|
1ba191c4ed | ||
|
|
6414903567 | ||
|
|
e62d8b4596 | ||
|
|
e27b1b1bc2 | ||
|
|
9507138f29 | ||
|
|
e5fa202a09 | ||
|
|
447098bc97 | ||
|
|
dc5e042cdd | ||
|
|
92fb365b54 | ||
|
|
161f5c7000 | ||
|
|
c8744768c4 | ||
|
|
b34a568a93 | ||
|
|
a669403604 | ||
|
|
7b2bfc2a64 | ||
|
|
7655c0c867 | ||
|
|
6fbabb4b26 | ||
|
|
edfc9ff082 | ||
|
|
59a9a77e3f | ||
|
|
8e756d7648 | ||
|
|
5763c1c2f1 | ||
|
|
6c5ae0b4d9 | ||
|
|
9cb9ce2e42 | ||
|
|
018be43017 | ||
|
|
4f720c8c9b | ||
|
|
06e1e113f0 | ||
|
|
3fc6f38da9 | ||
|
|
8e8c96c267 | ||
|
|
fab765c5ee | ||
|
|
89ed07a097 | ||
|
|
d7d73d2caf | ||
|
|
aa92730e28 | ||
|
|
430430e0cd | ||
|
|
62713940b0 | ||
|
|
75aef09338 | ||
|
|
4e65c30e18 | ||
|
|
8b873e2828 | ||
|
|
e70d179628 | ||
|
|
ca9a836300 | ||
|
|
abdf72dc0f | ||
|
|
227cd165f6 | ||
|
|
a0135cdc59 | ||
|
|
112d504148 | ||
|
|
d275930732 | ||
|
|
97e33bdd7e | ||
|
|
3f13791faf | ||
|
|
359cfc4be0 | ||
|
|
1ad6bba0ab | ||
|
|
6dc4211d81 | ||
|
|
e5f0452728 | ||
|
|
d95bba63f4 | ||
|
|
6f48869021 | ||
|
|
845bfae887 | ||
|
|
3fe02fba26 | ||
|
|
4ca5ca9fe2 | ||
|
|
364e187080 | ||
|
|
b1193eef1e | ||
|
|
e110f12cf1 | ||
|
|
022b513935 | ||
|
|
010f39a7a4 | ||
|
|
6b6eed66eb | ||
|
|
c214e01f03 | ||
|
|
a28ac3634d | ||
|
|
a0709fac88 | ||
|
|
e5b6a3332e | ||
|
|
979686920a | ||
|
|
793850f3aa | ||
|
|
fa9d72fc0a | ||
|
|
0d485ffed6 | ||
|
|
9a2e55d528 | ||
|
|
81ecbb86a5 | ||
|
|
2cd816932b | ||
|
|
d83411162d | ||
|
|
c9c54db3a1 | ||
|
|
d100600789 | ||
|
|
6f9011a167 | ||
|
|
ae004c8a6c | ||
|
|
ccbb16fe76 | ||
|
|
b77ce4a010 | ||
|
|
f72f709e34 | ||
|
|
37ceb25f8c | ||
|
|
0141d8f954 | ||
|
|
47fe791006 | ||
|
|
0cbaba73e4 | ||
|
|
aaac491dc7 | ||
|
|
fc4203e963 | ||
|
|
8430e9612e | ||
|
|
de81934619 | ||
|
|
5aedcbee55 | ||
|
|
7bc8d7e991 | ||
|
|
df183f5882 | ||
|
|
27d9fad292 | ||
|
|
bed798d440 | ||
|
|
e5a8a4e34d | ||
|
|
de446db434 | ||
|
|
81546412bf | ||
|
|
459530fba3 | ||
|
|
42766b42c0 | ||
|
|
38b3563350 | ||
|
|
37159914cf | ||
|
|
268bf68532 | ||
|
|
b02c7e01d5 | ||
|
|
45d10a3bc4 | ||
|
|
9250b20c1d | ||
|
|
d9da311141 | ||
|
|
89a52dad21 | ||
|
|
2a1acc1294 | ||
|
|
8302990f84 | ||
|
|
5a03b61d63 | ||
|
|
6b6f239936 | ||
|
|
9c2748e8e9 | ||
|
|
bb5cc33737 | ||
|
|
5616a64827 | ||
|
|
33530ad8c8 | ||
|
|
fe14839f1b | ||
|
|
15123c1aa5 | ||
|
|
71ee9c72fe | ||
|
|
5503539856 | ||
|
|
6548f01efa | ||
|
|
518a4484d6 | ||
|
|
3535a25a59 | ||
|
|
06cc6741e6 | ||
|
|
ed1478e1ba | ||
|
|
854c05d673 | ||
|
|
bcf5fb18c5 | ||
|
|
3f822e814b | ||
|
|
4fd25c7215 | ||
|
|
f42bed2a55 | ||
|
|
469c205033 | ||
|
|
31551f3b9e | ||
|
|
69925af17c | ||
|
|
2502868ebc | ||
|
|
38d242add9 | ||
|
|
c272e281cf | ||
|
|
d4e19b0fa4 | ||
|
|
c12877a58f | ||
|
|
af83350d04 | ||
|
|
9d0cf6be0b | ||
|
|
2fe9c782d4 | ||
|
|
d0a3d8d144 | ||
|
|
cefcb1eaf8 | ||
|
|
83b60de640 | ||
|
|
a076418989 | ||
|
|
64ecce1ccb | ||
|
|
cd56a5d4f0 | ||
|
|
d5ca121ac6 | ||
|
|
1e20e80b65 | ||
|
|
75ac3fd40c | ||
|
|
3b91cc7f02 | ||
|
|
23450b61e0 | ||
|
|
dcc226976c | ||
|
|
196fde72f6 | ||
|
|
1e913cd406 | ||
|
|
02425702d1 | ||
|
|
2494655369 | ||
|
|
0e0a78c47e | ||
|
|
ed02c10f54 | ||
|
|
9c40f76184 | ||
|
|
8eda30d88c | ||
|
|
9de19d35a8 | ||
|
|
0770403ca5 | ||
|
|
1a45cb35c1 | ||
|
|
15cdff76d9 | ||
|
|
8c4eb4b5fe | ||
|
|
ae30aafafe | ||
|
|
3b74717591 | ||
|
|
7a6349b4c0 | ||
|
|
3b51fff88d | ||
|
+5 |
2a0b0be9d0 | ||
|
|
f373c9d818 | ||
|
|
540bf3a1cb | ||
|
|
2d9064e92f | ||
|
|
9487819ec1 | ||
|
|
49bf14b196 | ||
|
|
344f16d6cc | ||
|
|
ccbf3b1e54 | ||
|
|
dcf0fad35d | ||
|
|
a836800c69 | ||
|
|
b8c6ac43f7 | ||
|
|
f5e0c6b384 | ||
|
|
2a5dd9035f | ||
|
|
2cddad4336 | ||
|
|
4bcbff51b1 | ||
|
|
ae541e4c37 | ||
|
|
f254711d85 | ||
|
|
533e208643 | ||
|
|
795574ef88 | ||
|
|
48611d5c99 | ||
|
|
11a07961d0 | ||
|
|
6be0fd52e7 | ||
|
|
78da7ba338 | ||
|
|
9a72c648c5 | ||
|
|
3e42da7fc7 | ||
|
|
1d986cad04 | ||
|
|
f603e4eff2 | ||
|
|
1f4e6c1987 | ||
|
|
e57f734e79 | ||
|
|
7fad76ae23 | ||
|
|
7659a016b4 | ||
|
|
34fe1c4a95 | ||
|
|
205f73c4bc | ||
|
|
c4bd2167e1 | ||
|
|
49a7111b66 | ||
|
|
ca045a040b | ||
|
|
a8b4198bf7 | ||
|
|
2ab24e530b | ||
|
|
734b06dfdf | ||
|
|
467084993b | ||
|
|
1e2c8516d2 | ||
|
|
d57738f325 | ||
|
|
c39a7dba85 | ||
|
|
fd37397e5c | ||
|
|
df0a522656 | ||
|
|
916468b8cc | ||
|
|
b789e58624 | ||
|
|
4368536141 | ||
|
|
9eef9766ce | ||
|
|
68dbfbd7d0 | ||
|
|
692a84a86e | ||
|
|
1671095c94 | ||
|
|
3b896bee5c | ||
|
|
cc1985442e | ||
|
|
630f438b32 | ||
|
|
6fae01d45a | ||
|
|
7009373b11 | ||
|
|
c2f150dbab | ||
|
|
688541923b | ||
|
|
20402ac26c | ||
|
|
a54952b41a | ||
|
|
987756c4d4 | ||
|
|
3ee294ffc5 | ||
|
|
a638708871 | ||
|
|
3456cc5521 | ||
|
|
546f0c3881 | ||
|
|
7b18272d60 | ||
|
|
cf6efabdbb | ||
|
|
92d15a00f5 | ||
|
|
2f2b72dd54 | ||
|
|
8fd5d6b5b5 | ||
|
|
2078f12a85 | ||
|
|
77b8eb8a7f | ||
|
|
463787b607 | ||
|
|
1f4e7ca695 | ||
|
|
9eb52bbfb2 | ||
|
|
61e6f76bc6 | ||
|
|
2e6c3a6068 | ||
|
|
8f48ecafec | ||
|
|
8849d1148c | ||
|
|
98a4684a2b | ||
|
|
da0bdfacae | ||
|
|
68c7425922 | ||
|
|
c3418cbe1f | ||
|
|
3a641e0bc1 | ||
|
|
b7fcf6ade1 | ||
|
|
53d160e495 | ||
|
|
df4df6846b | ||
|
|
c1bbb9b139 | ||
|
|
56c07ec351 | ||
|
|
3080b47aec | ||
|
|
c1e23bfdc6 | ||
|
|
e7ff5508d7 | ||
|
|
5850d227ee | ||
|
|
ede987ba19 | ||
|
|
bc0d83164c | ||
|
|
30f0b080c5 | ||
|
|
ee39ea316f | ||
|
|
fbc2708259 | ||
|
|
9bb83aba7e |
@@ -0,0 +1,42 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker
|
||||
{
|
||||
"name": "Docker outside of Docker",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-from-docker:1": {
|
||||
"version": "latest",
|
||||
"enableNonRootDocker": "true",
|
||||
"moby": "true"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/npm-package:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/jest:2": {},
|
||||
"ghcr.io/devcontainers-contrib/features/prisma:2": {},
|
||||
"ghcr.io/guiyomh/features/vim:0": {}
|
||||
},
|
||||
|
||||
// Use this environment variable if you need to bind mount your local source code into a new container.
|
||||
"remoteEnv": {
|
||||
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
|
||||
},
|
||||
|
||||
"hostRequirements": {
|
||||
"cpus": 4,
|
||||
"memory": "8gb"
|
||||
},
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "./deploy/install.sh"
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
+16
-4
@@ -7,14 +7,13 @@
|
||||
# - E-MAIL SETTINGS
|
||||
|
||||
# - LICENSE *************************************************************************************************
|
||||
# Set this value to 'agree' to accept our license:
|
||||
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
|
||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||
#
|
||||
# Summary of terms:
|
||||
# - The codebase has to stay open source, whether it was modified or not
|
||||
# - You can not repackage or sell the codebase
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||
#
|
||||
# To enable enterprise-only features, fill your license key in here.
|
||||
# @see https://console.cal.com
|
||||
CALCOM_LICENSE_KEY=
|
||||
@@ -32,11 +31,14 @@ NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||
|
||||
# To enable SAML login, set both these variables
|
||||
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login
|
||||
# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
|
||||
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
|
||||
SAML_DATABASE_URL=
|
||||
# SAML_ADMINS='pro@example.com'
|
||||
SAML_ADMINS=
|
||||
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
|
||||
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
|
||||
|
||||
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
|
||||
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||
# PGSSLMODE='no-verify'
|
||||
@@ -80,12 +82,17 @@ SEND_FEEDBACK_EMAIL=
|
||||
# Used for email reminders in workflows and internal sync services
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_EMAIL=
|
||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||
|
||||
# Twilio
|
||||
# Used to send SMS reminders in workflows
|
||||
TWILIO_SID=
|
||||
TWILIO_TOKEN=
|
||||
TWILIO_MESSAGING_SID=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
||||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
@@ -100,6 +107,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
PAYMENT_FEE_PERCENTAGE=
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
@@ -145,3 +154,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||
# Set this to true in to disable new signups
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
"♻️ autoupdate":
|
||||
- apps/web/public/static/locales/**/*.json
|
||||
"❗️ migrations":
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
|
||||
automerge:
|
||||
- apps/web/public/static/locales/**/*.json
|
||||
"❗️ .env changes":
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
|
||||
@@ -9,6 +9,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
required-checks:
|
||||
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,6 +23,7 @@ jobs:
|
||||
with:
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
pull_request_labels: "♻️ autoupdate, automerge"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/embeds/**
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
name: E2E test
|
||||
on:
|
||||
push:
|
||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/**
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -12,8 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
||||
if: ${{ github.actor == 'github-actions[bot]' }}
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
|
||||
@@ -11,7 +11,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
name: "Meta Workflow: Require Conditional Status Checks"
|
||||
on:
|
||||
push:
|
||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
name: Unit tests
|
||||
on:
|
||||
push:
|
||||
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/**
|
||||
workflow_run:
|
||||
workflows: [Crowdin Action]
|
||||
types: [completed]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -80,3 +80,10 @@ apps/storybook/build-storybook.log
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.snaplet/structure.d.ts
|
||||
|
||||
# Submodules
|
||||
.gitmodules
|
||||
apps/api
|
||||
apps/website
|
||||
apps/console
|
||||
apps/auth
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
[submodule "apps/console"]
|
||||
path = apps/console
|
||||
url = https://github.com/calcom/console.git
|
||||
branch = main
|
||||
[submodule "apps/api"]
|
||||
path = apps/api
|
||||
url = https://github.com/calcom/api.git
|
||||
branch = main
|
||||
[submodule "apps/website"]
|
||||
path = apps/website
|
||||
url = https://github.com/calcom/website.git
|
||||
branch = main
|
||||
@@ -33,11 +33,6 @@
|
||||
"generator": "secret",
|
||||
"required": "true"
|
||||
},
|
||||
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
||||
"description": "License consent",
|
||||
"value": "true",
|
||||
"required": "true"
|
||||
},
|
||||
"CRON_API_KEY": {
|
||||
"description": "ApiKey for cronjobs",
|
||||
"value": ""
|
||||
|
||||
-1
Submodule apps/api deleted from 8b74f463f4
-1
Submodule apps/console deleted from 564f9b2faa
@@ -1,81 +1 @@
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="right">
|
||||
<a href="https://github.com/calcom/cal.com">
|
||||
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
|
||||
</a>
|
||||
<a href="https://cal.com">Website</a>
|
||||
·
|
||||
<a href="https://github.com/calcom/cal.com/issues">Community Support</a>
|
||||
</div>
|
||||
|
||||
# Cal.com Documentation
|
||||
|
||||
The official product, support and developer documentation, containing information and guides about using the product as well as support for self-hosted installations. This documentation site runs on [Nextra](https://nextra.vercel.app), so you may refer to their documentation should you need information on anything that isn't covered here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
- Node.js & npm
|
||||
- Yarn
|
||||
|
||||
## Installation
|
||||
|
||||
Firstly, clone the repository using Git:
|
||||
|
||||
```console
|
||||
git clone https://github.com/calcom/docs.git
|
||||
```
|
||||
|
||||
Now, you can install the dependencies with yarn:
|
||||
|
||||
```console
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Editing
|
||||
|
||||
To create, edit and delete documentation pages, you can simply create markdown (.mdx) files in the `pages/` folder. You can edit Markdown with any text editor, but VS Code and WebStorm have side-by-side previews so you can see your formatted content whilst writing markdown.
|
||||
|
||||
You will also need to add it as an entry to the `meta.json` file found in whichever directory that the .mdx file is in.
|
||||
|
||||
## Local Development
|
||||
|
||||
```console
|
||||
yarn dev
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```console
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static content hosting service.
|
||||
|
||||
## How to easily contribute
|
||||
|
||||
## Existing Page
|
||||
|
||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
||||
2. From here you can view all current pages on the documentation site. Select the page you would like to contribute to.
|
||||
3. You should now be able to view the page you have selected. Located at the top right of the page will be a pencil icon. Pressing this will bring you up an editor to edit and make changes. You can add formatting using the buttons at the top, which will automatically insert the relevant markdown content needed to style the text.
|
||||
4. From here make the changes you wish to make.
|
||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
||||
6. Your changes have been saved, to submit them for review, located on your screen, press the green 'Create Pull Request' button.
|
||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
||||
|
||||
You have now successfully edited and submitted changes to our documentation site.
|
||||
|
||||
## Creating a New Page
|
||||
|
||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
||||
2. From here you can view all current pages on the documentation site. At the top of your screen press the 'New file' button.
|
||||
3. You should now be able to view the page you have created. Remember when renaming the document to put .mdx at the end of the file name.
|
||||
4. From here make the changes you wish to make. Such as creating a title, sub-title and body text.
|
||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
||||
6. Your changes have been saved, to submit them for review, located on your screen, press the greem 'Create Pull Request' button.
|
||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
||||
|
||||
You have now successfully created and submitted changes to our documentation site.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iframe-resizer-react": "^1.1.0",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"nextra": "^1.1.0",
|
||||
"nextra-theme-docs": "^1.2.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -14,14 +14,13 @@ Cal.com uses [TailwindCSS](https://tailwindcss.com) as a replacement for traditi
|
||||
|
||||
CSS files should be stored in the `styles` directory within the codebase.
|
||||
|
||||
Within the `styles` directory, you will find two CSS files, `fonts.css` and `global.css`. We suggest not to add to these files, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
|
||||
Within the `styles` directory, you will find a single CSS file, `global.css`. We suggest not to add to this file, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
|
||||
|
||||
## Adding new stylesheets
|
||||
Firstly, create the CSS file inside the `styles` directory.
|
||||
|
||||
Then, open the `pages/_app.tsx` file, and you will see the following two lines:
|
||||
```javascript
|
||||
import "../styles/fonts.css";
|
||||
import "../styles/globals.css";
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
|
||||
| ├──index.ts
|
||||
| ├──package.json
|
||||
| ├──.env.example
|
||||
| ├──README.mdx
|
||||
| ├──DESCRIPTION.md
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
|
||||
|
||||
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
||||
|
||||
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead.
|
||||
On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
|
||||
|
||||
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use.
|
||||
The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
|
||||
|
||||
## Adding Your App to the App Store
|
||||
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
||||
|
||||
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
storybook-static
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@@ -21,4 +22,4 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
@@ -4,19 +4,22 @@ module.exports = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/features/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-rtl-direction",
|
||||
"storybook-react-i18next",
|
||||
{
|
||||
"storybook-addon-next",
|
||||
/*{
|
||||
name: "storybook-addon-next",
|
||||
options: {
|
||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
||||
},
|
||||
},
|
||||
},*/
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
@@ -50,6 +53,21 @@ module.exports = {
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,11 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
|
||||
addDecorator((storyFn) => (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||
</I18nextProvider>
|
||||
));
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
||||
|
||||
const withTM = require("next-transpile-modules")([
|
||||
"@calcom/app-store",
|
||||
"@calcom/dayjs",
|
||||
"@calcom/emails",
|
||||
"@calcom/trpc",
|
||||
"@calcom/embed-core",
|
||||
"@calcom/embed-react",
|
||||
"@calcom/features",
|
||||
"@calcom/lib",
|
||||
"@calcom/prisma",
|
||||
"@calcom/ui",
|
||||
]);
|
||||
const glob = require("glob");
|
||||
|
||||
const plugins = [];
|
||||
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
domains: ["www.datocms-assets.com"],
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: { images: { allowFutureImage: true } },
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
@@ -20,9 +20,10 @@
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook-addon-rtl-direction": "^0.0.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
@@ -48,7 +49,7 @@
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^2.9.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "../../../packages/ui/styles/shared-globals.css"
|
||||
@import "../../../packages/ui/styles/shared-globals.css"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||
|
||||
.sbdocs {
|
||||
font-family: 'Inter var' !important;
|
||||
padding: 0!important;
|
||||
@@ -36,7 +38,7 @@
|
||||
|
||||
/** Docs table **/
|
||||
.custom-args-wrapper{
|
||||
max-height: 600px;
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 1rem;
|
||||
@@ -187,3 +189,7 @@
|
||||
padding: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-story {
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -24,6 +24,6 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
14.17
|
||||
v16
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge, ListItemText } from "@calcom/ui";
|
||||
|
||||
interface AppListCardProps {
|
||||
logo?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export default function AppListCard(props: AppListCardProps) {
|
||||
const { t } = useLocale();
|
||||
const { logo, title, description, actions, isDefault } = props;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
|
||||
</div>
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
</div>
|
||||
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Suspense, SuspenseProps } from "react";
|
||||
|
||||
/**
|
||||
* Wrapper around `<Suspense />` which will render the `fallback` when on server
|
||||
* Can be simply replaced by `<Suspense />` once React 18 is ready.
|
||||
*/
|
||||
export const ClientSuspense = (props: SuspenseProps) => {
|
||||
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
|
||||
};
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
InputLeading,
|
||||
Label,
|
||||
showToast,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
TextArea,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
import Select from "@components/ui/form/Select";
|
||||
@@ -44,6 +44,7 @@ type PreviewState = {
|
||||
palette: {
|
||||
brandColor: string;
|
||||
};
|
||||
hideEventTypeDetails: boolean;
|
||||
};
|
||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||
|
||||
@@ -125,10 +126,12 @@ const getEmbedUIInstructionString = ({
|
||||
apiName,
|
||||
theme,
|
||||
brandColor,
|
||||
hideEventTypeDetails,
|
||||
}: {
|
||||
apiName: string;
|
||||
theme?: string;
|
||||
brandColor: string;
|
||||
hideEventTypeDetails: boolean;
|
||||
}) => {
|
||||
theme = theme !== "auto" ? theme : undefined;
|
||||
return getInstructionString({
|
||||
@@ -141,6 +144,7 @@ const getEmbedUIInstructionString = ({
|
||||
brandColor,
|
||||
},
|
||||
},
|
||||
hideEventTypeDetails: hideEventTypeDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -253,18 +257,21 @@ const getEmbedTypeSpecificString = ({
|
||||
apiName: string;
|
||||
theme: PreviewState["theme"];
|
||||
brandColor: string;
|
||||
hideEventTypeDetails: boolean;
|
||||
};
|
||||
if (embedFramework === "react") {
|
||||
uiInstructionStringArg = {
|
||||
apiName: "cal",
|
||||
theme: previewState.theme,
|
||||
brandColor: previewState.palette.brandColor,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
};
|
||||
} else {
|
||||
uiInstructionStringArg = {
|
||||
apiName: "Cal",
|
||||
theme: previewState.theme,
|
||||
brandColor: previewState.palette.brandColor,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
};
|
||||
}
|
||||
if (!frameworkCodes[embedType]) {
|
||||
@@ -481,7 +488,7 @@ const tabs = [
|
||||
{
|
||||
name: "HTML",
|
||||
href: "embedTabName=embed-code",
|
||||
icon: Icon.FiCode,
|
||||
icon: FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -497,7 +504,7 @@ const tabs = [
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<small className="flex py-4 text-neutral-500">
|
||||
<small className="flex py-4 text-gray-500">
|
||||
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||
</small>
|
||||
</div>
|
||||
@@ -534,7 +541,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
{
|
||||
name: "React",
|
||||
href: "embedTabName=embed-react",
|
||||
icon: Icon.FiCode,
|
||||
icon: FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -549,7 +556,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<small className="flex py-4 text-neutral-500">{t("create_update_react_component")}</small>
|
||||
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
|
||||
<TextArea
|
||||
data-testid="embed-react"
|
||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||
@@ -574,7 +581,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
||||
{
|
||||
name: "Preview",
|
||||
href: "embedTabName=embed-preview",
|
||||
icon: Icon.FiTrello,
|
||||
icon: FiTrello,
|
||||
type: "iframe",
|
||||
Component: forwardRef<
|
||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||
@@ -590,7 +597,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
||||
<iframe
|
||||
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
|
||||
data-testid="embed-preview"
|
||||
className="border-1 h-[100vh] border"
|
||||
className="h-[100vh] border"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
||||
@@ -610,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||
return (
|
||||
<components.Control {...props}>
|
||||
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||
{children}
|
||||
</components.Control>
|
||||
);
|
||||
@@ -632,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
<div className="flex items-start">
|
||||
{embeds.map((embed, index) => (
|
||||
<button
|
||||
className="mr-2 w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100"
|
||||
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
|
||||
key={index}
|
||||
data-testid={embed.type}
|
||||
onClick={() => {
|
||||
@@ -643,7 +650,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
||||
{embed.illustration}
|
||||
</div>
|
||||
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div>
|
||||
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
|
||||
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
||||
</button>
|
||||
))}
|
||||
@@ -689,6 +696,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
theme: Theme.auto,
|
||||
floatingPopup: {},
|
||||
elementClick: {},
|
||||
hideEventTypeDetails: false,
|
||||
palette: {
|
||||
brandColor: "#000000",
|
||||
},
|
||||
@@ -753,6 +761,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
name: "ui",
|
||||
arg: {
|
||||
theme: previewState.theme,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
styles: {
|
||||
branding: {
|
||||
...previewState.palette,
|
||||
@@ -806,19 +815,19 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
onClick={() => {
|
||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||
}}>
|
||||
<Icon.FiArrowLeft className="mr-4 w-4" />
|
||||
<FiArrowLeft className="mr-4 w-4" />
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||
<div className="max-h-97 flex flex-col overflow-y-auto">
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
className="flex w-full items-center text-base font-medium text-neutral-900">
|
||||
className="flex w-full items-center text-base font-medium text-gray-900">
|
||||
<div>
|
||||
{embedType === "inline"
|
||||
? "Inline Embed Customization"
|
||||
@@ -826,10 +835,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
? "Floating Popup Customization"
|
||||
: "Element Click Customization"}
|
||||
</div>
|
||||
<Icon.FiChevronRight
|
||||
<FiChevronRight
|
||||
className={`${
|
||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
} ml-auto h-5 w-5 text-gray-500`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="text-sm">
|
||||
@@ -838,7 +847,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="text-sm">Embed Window Sizing</div>
|
||||
<div className="justify-left flex items-center">
|
||||
<TextField
|
||||
name="width"
|
||||
labelProps={{ className: "hidden" }}
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
@@ -860,7 +868,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<span className="p-2">×</span>
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
name="height"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
onChange={(e) => {
|
||||
@@ -888,7 +895,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="mb-2 text-sm">Button Text</div>
|
||||
{/* Default Values should come from preview iframe */}
|
||||
<TextField
|
||||
name="buttonText"
|
||||
labelProps={{ className: "hidden" }}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -908,7 +914,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 flex items-center justify-start",
|
||||
embedType === "floating-popup" ? "space-x-2" : "hidden"
|
||||
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
|
||||
)}>
|
||||
<Switch
|
||||
defaultChecked={true}
|
||||
@@ -995,15 +1001,29 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
open={isBookingCustomizationOpen}
|
||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger className="flex w-full" type="button">
|
||||
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
||||
<Icon.FiChevronRight
|
||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
||||
<FiChevronRight
|
||||
className={`${
|
||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
} ml-auto h-5 w-5 text-gray-500`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-6 text-sm">
|
||||
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||
<Switch
|
||||
checked={previewState.hideEventTypeDetails}
|
||||
onCheckedChange={(checked) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
hideEventTypeDetails: checked,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||
</div>
|
||||
<Label className="">
|
||||
<div className="mb-2">Theme</div>
|
||||
<Select
|
||||
@@ -1143,7 +1163,7 @@ export const EmbedButton = <T extends React.ElementType>({
|
||||
...props
|
||||
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
|
||||
const router = useRouter();
|
||||
className = classNames(className, "hidden lg:inline-flex");
|
||||
className = classNames("hidden lg:inline-flex", className);
|
||||
const openEmbedModal = () => {
|
||||
goto(router, {
|
||||
dialog: "embed",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n() {
|
||||
@@ -21,12 +22,16 @@ export function useViewerI18n() {
|
||||
*/
|
||||
const I18nLanguageHandler = (): null => {
|
||||
const { i18n } = useTranslation("common");
|
||||
const locale = useViewerI18n().data?.locale;
|
||||
const locale = useViewerI18n().data?.locale || "en";
|
||||
|
||||
useEffect(() => {
|
||||
if (locale && i18n.language && i18n.language !== locale) {
|
||||
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
|
||||
}
|
||||
|
||||
const dir = getDirFromLang(locale);
|
||||
document.documentElement.setAttribute("lang", locale);
|
||||
document.documentElement.setAttribute("dir", dir);
|
||||
}, [locale, i18n]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function ImageUploader({
|
||||
</div>
|
||||
)}
|
||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
||||
<input
|
||||
onInput={onInputFile}
|
||||
type="file"
|
||||
|
||||
@@ -62,12 +62,12 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
const className = tab.className || "";
|
||||
return (
|
||||
<Component key={tab.name}>
|
||||
<Link key={tab.name} href={href} {...linkProps}>
|
||||
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
|
||||
<a
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
isCurrent
|
||||
? "border-neutral-900 text-neutral-900"
|
||||
? "border-gray-900 text-gray-900"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||
className
|
||||
@@ -76,7 +76,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
{tab.icon && (
|
||||
<tab.icon
|
||||
className={classNames(
|
||||
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
||||
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
||||
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import { ErrorBoundary, Icon, Shell } from "@calcom/ui";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
|
||||
@@ -8,32 +10,32 @@ const tabs = [
|
||||
{
|
||||
name: "profile",
|
||||
href: "/settings/profile",
|
||||
icon: Icon.FiUser,
|
||||
icon: FiUser,
|
||||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
icon: Icon.FiUsers,
|
||||
icon: FiUsers,
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
href: "/settings/security",
|
||||
icon: Icon.FiKey,
|
||||
icon: FiKey,
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
href: "/settings/developer",
|
||||
icon: Icon.FiTerminal,
|
||||
icon: FiTerminal,
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: Icon.FiCreditCard,
|
||||
icon: FiCreditCard,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
href: "/settings/admin",
|
||||
icon: Icon.FiLock,
|
||||
icon: FiLock,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,7 +4,8 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Button, Icon, Select } from "@calcom/ui";
|
||||
import { Button, Select } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -31,7 +32,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
||||
/>
|
||||
) : (
|
||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
||||
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||
<p>{t("install_new_calendar_app")}</p>
|
||||
</Button>
|
||||
);
|
||||
@@ -61,7 +62,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
<Select
|
||||
name="additionalCalendar"
|
||||
placeholder={
|
||||
<Button StartIcon={Icon.FiPlus} color="secondary">
|
||||
<Button StartIcon={FiPlus} color="secondary">
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -6,14 +6,23 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App as AppType } from "@calcom/types/App";
|
||||
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import HeadSeo from "@components/seo/head-seo";
|
||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||
import {
|
||||
FiBookOpen,
|
||||
FiCheck,
|
||||
FiExternalLink,
|
||||
FiFile,
|
||||
FiFlag,
|
||||
FiMail,
|
||||
FiPlus,
|
||||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
@@ -35,6 +44,7 @@ const Component = ({
|
||||
privacy,
|
||||
isProOnly,
|
||||
images,
|
||||
isTemplate,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const hasImages = images && images.length > 0;
|
||||
@@ -66,7 +76,9 @@ const Component = ({
|
||||
}
|
||||
);
|
||||
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||
// Such apps, can only be installed once.
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||
@@ -98,18 +110,25 @@ const Component = ({
|
||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-gray-600">
|
||||
<Link href={`categories/${categories[0]}`}>
|
||||
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a>
|
||||
<Link
|
||||
href={`categories/${categories[0]}`}
|
||||
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
||||
{categories[0]}
|
||||
</Link>{" "}
|
||||
• {t("published_by", { author })}
|
||||
</h2>
|
||||
{isTemplate && (
|
||||
<Badge variant="red" className="mt-4">
|
||||
Template - Available in Dev Environment only for testing
|
||||
</Badge>
|
||||
)}
|
||||
</header>
|
||||
</div>
|
||||
{!appCredentials.isLoading ? (
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("default")}
|
||||
@@ -130,7 +149,7 @@ const Component = ({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
{...props}
|
||||
// @TODO: Overriding color and size prevent us from
|
||||
// having to duplicate InstallAppButton for now.
|
||||
@@ -191,7 +210,7 @@ const Component = ({
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="prose prose-sm mt-8">{body}</div>
|
||||
<div className="prose-sm prose mt-8">{body}</div>
|
||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||
<span>
|
||||
{price === 0 ? (
|
||||
@@ -217,7 +236,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-sm font-normal text-black no-underline hover:underline"
|
||||
href={docs}>
|
||||
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
||||
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -229,7 +248,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={website}>
|
||||
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -241,7 +260,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -254,7 +273,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={tos}>
|
||||
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -266,7 +285,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -277,7 +296,7 @@ const Component = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,19 +326,16 @@ export default function App(props: {
|
||||
licenseRequired: AppType["licenseRequired"];
|
||||
isProOnly: AppType["isProOnly"];
|
||||
images?: string[];
|
||||
isTemplate?: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
||||
<Shell smallHeading isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
||||
<HeadSeo
|
||||
title={props.name}
|
||||
description={props.description}
|
||||
app={{ slug: props.logo, name: props.name, description: props.description }}
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
{props.licenseRequired ? (
|
||||
<LicenseRequired>
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
List,
|
||||
ShellSubHeading,
|
||||
showToast,
|
||||
SkeletonLoader,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
Switch,
|
||||
ShellSubHeading,
|
||||
} from "@calcom/ui";
|
||||
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
|
||||
/>
|
||||
{!!props.destination && (
|
||||
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
{t("adding_events_to")}
|
||||
</span>
|
||||
)}
|
||||
@@ -159,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
if (!data.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
@@ -168,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
slug={item.integration.slug}
|
||||
title={item.integration.title}
|
||||
logo={item.integration.logo}
|
||||
description={item.primary?.externalId || "No external Id"}
|
||||
description={item.primary?.email ?? item.integration.description}
|
||||
separate={true}
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
@@ -183,9 +184,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
<div className="border-t border-gray-200">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="px-4 pt-4 text-sm text-neutral-500">
|
||||
{t("toggle_calendars_conflict")}
|
||||
</p>
|
||||
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 p-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
@@ -283,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
|
||||
<Icon.FiCalendar className="h-8 w-8" strokeWidth="1" />
|
||||
<Icon.FiPlus
|
||||
<FiCalendar className="h-8 w-8" strokeWidth="1" />
|
||||
<FiPlus
|
||||
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
@@ -323,13 +322,16 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiCalendar}
|
||||
Icon={FiCalendar}
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
description={t(`no_category_apps_description_calendar`)}
|
||||
buttonRaw={
|
||||
<Button color="secondary" href="/apps/categories/calendar">
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="connect-calendar-apps"
|
||||
href="/apps/categories/calendar">
|
||||
{t(`connect_calendar_apps`)}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
||||
import { Badge, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -19,6 +20,7 @@ function IntegrationListItem(props: {
|
||||
destination?: boolean;
|
||||
separate?: boolean;
|
||||
invalidCredential?: boolean;
|
||||
isTemplate?: boolean;
|
||||
}): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -44,20 +46,25 @@ function IntegrationListItem(props: {
|
||||
expanded={!!props.children}
|
||||
className={classNames(
|
||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
||||
"my-0 flex-col border transition-colors duration-500 ",
|
||||
"my-0 flex-col border transition-colors duration-500",
|
||||
highlight ? "bg-yellow-100" : ""
|
||||
)}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3">
|
||||
<ListItemTitle component="h3" className="flex ">
|
||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
||||
{props.isTemplate && (
|
||||
<Badge variant="red" className="ml-4">
|
||||
Template
|
||||
</Badge>
|
||||
)}
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
{/* Alert error that key stopped working. */}
|
||||
{props.invalidCredential && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
||||
<div className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||
<FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
|
||||
+5
-21
@@ -2,23 +2,10 @@ import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen, Icon } from "@calcom/ui";
|
||||
|
||||
import Shell from "../Shell";
|
||||
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
|
||||
import HorizontalTabs from "../navigation/tabs/HorizontalTabs";
|
||||
|
||||
const tabs: HorizontalTabItemProps[] = [
|
||||
{
|
||||
name: "app_store",
|
||||
href: "/apps",
|
||||
},
|
||||
{
|
||||
name: "installed_apps",
|
||||
href: "/apps/installed",
|
||||
},
|
||||
];
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -34,15 +21,12 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
if (session.status === "loading") return <></>;
|
||||
|
||||
return (
|
||||
<Shell {...rest} actions={actions?.("hidden sm:block")}>
|
||||
<Shell {...rest} actions={actions?.("block")}>
|
||||
<div className="flex flex-col xl:flex-row">
|
||||
<div className="block lg:hidden">
|
||||
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
|
||||
</div>
|
||||
<main className="w-full">
|
||||
{emptyStore ? (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiAlertCircle}
|
||||
Icon={FiAlertCircle}
|
||||
headline={t("no_apps")}
|
||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||
+17
-14
@@ -2,43 +2,41 @@ import React, { ComponentProps } from "react";
|
||||
|
||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { Icon } from "../../../Icon";
|
||||
import Shell from "../Shell";
|
||||
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
|
||||
import type { VerticalTabItemProps } from "../navigation/tabs/VerticalTabItem";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
|
||||
|
||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||
{
|
||||
name: "calendar",
|
||||
href: "/apps/installed/calendar",
|
||||
icon: Icon.FiCalendar,
|
||||
icon: FiCalendar,
|
||||
},
|
||||
{
|
||||
name: "conferencing",
|
||||
href: "/apps/installed/conferencing",
|
||||
icon: Icon.FiVideo,
|
||||
icon: FiVideo,
|
||||
},
|
||||
{
|
||||
name: "payment",
|
||||
href: "/apps/installed/payment",
|
||||
icon: Icon.FiCreditCard,
|
||||
icon: FiCreditCard,
|
||||
},
|
||||
{
|
||||
name: "automation",
|
||||
href: "/apps/installed/automation",
|
||||
icon: Icon.FiShare2,
|
||||
icon: FiShare2,
|
||||
},
|
||||
{
|
||||
name: "analytics",
|
||||
href: "/apps/installed/analytics",
|
||||
icon: Icon.FiBarChart,
|
||||
icon: FiBarChart,
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
href: "/apps/installed/other",
|
||||
icon: Icon.FiGrid,
|
||||
icon: FiGrid,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -46,17 +44,22 @@ export default function InstalledAppsLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const variant: typeof InstalledAppVariants[number] = "payment";
|
||||
|
||||
const query = trpc.viewer.integrations.useQuery({
|
||||
variant: InstalledAppVariants.payment,
|
||||
variant,
|
||||
onlyInstalled: true,
|
||||
});
|
||||
let actualTabs = tabs;
|
||||
if (query.data?.items.length === 0) {
|
||||
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
|
||||
actualTabs = tabs.filter((tab) => tab.name !== variant);
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell {...rest}>
|
||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="w-full xl:mx-5 xl:w-4/5 xl:pr-5">
|
||||
<AppCategoryNavigation
|
||||
baseURL="/apps/installed"
|
||||
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
|
||||
{children}
|
||||
</AppCategoryNavigation>
|
||||
</Shell>
|
||||
+4
-3
@@ -2,9 +2,10 @@ import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { ComponentProps, useEffect } from "react";
|
||||
|
||||
import ErrorBoundary from "../../../ErrorBoundary";
|
||||
import Shell from "../Shell";
|
||||
import SettingsLayout from "./SettingsLayout";
|
||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
|
||||
import { UserPermissionRole } from ".prisma/client";
|
||||
|
||||
export default function AdminLayout({
|
||||
@@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { Icon, Tooltip } from "@calcom/ui";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
import { FiLink } from "@calcom/ui/components/icon";
|
||||
|
||||
import { Props } from "./pages/AvailabilityPage";
|
||||
|
||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return locations.length ? (
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||
{locations.map((location) => {
|
||||
@@ -16,19 +20,19 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
return (
|
||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<Icon.FiLink className="dark:text-darkgray-600 mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 " />
|
||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={locationKeyToString(location)}>
|
||||
<p className="truncate">{locationKeyToString(location)}</p>
|
||||
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
||||
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
@@ -71,7 +71,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
{slots.length > 0 &&
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
@@ -87,7 +87,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
slug: eventTypeSlug,
|
||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||
ethSignature,
|
||||
...(ethSignature ? { ethSignature } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={dayjs(slot.time).format()}>
|
||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||
{/* Current there is no way to disable Next.js Links */}
|
||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||
<div
|
||||
@@ -113,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={bookingUrl} prefetch={false}>
|
||||
<a
|
||||
className={classNames(
|
||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</a>
|
||||
<Link
|
||||
href={bookingUrl}
|
||||
prefetch={false}
|
||||
className={classNames(
|
||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { FC, ReactNode, useEffect } from "react";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, Badge } from "@calcom/ui";
|
||||
import { Badge } from "@calcom/ui";
|
||||
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
@@ -44,18 +45,17 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
const { profile, eventType, isBookingPage = false, children } = props;
|
||||
const { date: bookingDate } = useRouterQuery("date");
|
||||
const { t } = useLocale();
|
||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
||||
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.metadata?.multipleDuration !== undefined) {
|
||||
if (!duration) {
|
||||
setDuration(eventType.length);
|
||||
} else {
|
||||
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
|
||||
setDuration(eventType.length);
|
||||
}
|
||||
}
|
||||
if (
|
||||
eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||
) {
|
||||
setDuration(eventType.length.toString());
|
||||
}
|
||||
}, []);
|
||||
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||
|
||||
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||
let requiresConfirmationText = t("requires_confirmation");
|
||||
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||
@@ -77,7 +77,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
profile={profile}
|
||||
users={eventType.users}
|
||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||
size={10}
|
||||
size="sm"
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
@@ -94,9 +94,9 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiInfo
|
||||
<FiInfo
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] inline-block h-4 w-4",
|
||||
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
||||
)}
|
||||
/>
|
||||
@@ -113,7 +113,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
||||
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
</div>
|
||||
{requiresConfirmationText}
|
||||
</div>
|
||||
@@ -124,11 +124,12 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-nowrap text-sm font-medium",
|
||||
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
||||
isBookingPage && "dark:text-darkgray-600 text-gray-600",
|
||||
!eventType.metadata?.multipleDuration && "items-center"
|
||||
)}>
|
||||
<Icon.FiClock
|
||||
<FiClock
|
||||
className={classNames(
|
||||
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
|
||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "mt-[2px]"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -17,20 +18,21 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
MeetingTimeInTimezones,
|
||||
showToast,
|
||||
TextArea,
|
||||
Tooltip,
|
||||
ActionType,
|
||||
TableActions,
|
||||
} from "@calcom/ui";
|
||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
@@ -48,6 +50,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.status === BookingStatus.REJECTED) {
|
||||
@@ -97,7 +100,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: Icon.FiSlash,
|
||||
icon: FiSlash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
{
|
||||
@@ -106,12 +109,23 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: Icon.FiCheck,
|
||||
icon: FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
color: "primary",
|
||||
},
|
||||
];
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
{
|
||||
id: "view_recordings",
|
||||
label: t("view_recordings"),
|
||||
onClick: () => {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
];
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
@@ -121,7 +135,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}`,
|
||||
icon: Icon.FiX,
|
||||
icon: FiX,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
@@ -129,13 +143,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: Icon.FiClock,
|
||||
icon: FiClock,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: Icon.FiSend,
|
||||
icon: FiSend,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
@@ -148,7 +162,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: Icon.FiMapPin,
|
||||
icon: FiMapPin,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -158,10 +172,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
|
||||
if (isPast && isPending && !isConfirmed) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||
}
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -202,7 +220,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -216,7 +235,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isOpenDialog={isOpenSetLocationDialog}
|
||||
setShowLocationModal={setIsOpenLocationDialog}
|
||||
/>
|
||||
|
||||
{showRecordingsButtons && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||
setIsOpenDialog={setViewRecordingsDialogIsOpen}
|
||||
timeFormat={user?.timeFormat ?? null}
|
||||
/>
|
||||
)}
|
||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
<DialogContent>
|
||||
@@ -248,7 +274,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
|
||||
<td
|
||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||
onClick={onClickTableData}>
|
||||
@@ -275,11 +301,6 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{booking.eventType.team.name}
|
||||
</Badge>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
{booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||
{t("paid")}
|
||||
@@ -336,19 +357,21 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<div
|
||||
title={booking.title}
|
||||
className={classNames(
|
||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full",
|
||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
||||
isCancelled ? "line-through" : ""
|
||||
)}>
|
||||
{booking.title}
|
||||
<span> </span>
|
||||
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{booking.description && (
|
||||
<div
|
||||
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
||||
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
||||
title={booking.description}>
|
||||
"{booking.description}"
|
||||
</div>
|
||||
@@ -376,6 +399,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</>
|
||||
) : null}
|
||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="hidden h-full items-center md:flex">
|
||||
<RequestSentMessage />
|
||||
@@ -431,7 +455,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||
);
|
||||
})}>
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCcw
|
||||
<FiRefreshCcw
|
||||
strokeWidth="3"
|
||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
||||
/>
|
||||
@@ -531,13 +555,4 @@ const DisplayAttendees = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingListItem;
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
import { Button, TextArea } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -39,7 +40,7 @@ export default function CancelBooking(props: Props) {
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
<FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
@@ -55,7 +56,7 @@ export default function CancelBooking(props: Props) {
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
|
||||
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
|
||||
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
||||
|
||||
export const UserAvatars = ({
|
||||
profile,
|
||||
@@ -14,7 +13,6 @@ export const UserAvatars = ({
|
||||
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
|
||||
return (
|
||||
<AvatarGroup
|
||||
border="border-2 dark:border-darkgray-100 border-white"
|
||||
items={
|
||||
[
|
||||
{
|
||||
@@ -38,7 +36,7 @@ export const UserAvatars = ({
|
||||
href?: string;
|
||||
}[]
|
||||
}
|
||||
size={props.size}
|
||||
size="sm"
|
||||
truncateAfter={props.truncateAfter}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,11 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useEmbedStyles,
|
||||
useEmbedUiConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
@@ -26,7 +28,8 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon, DatePicker } from "@calcom/ui";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiChevronDown, FiChevronUp, FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -35,7 +38,6 @@ import Gates, { Gate, GateState } from "@components/Gates";
|
||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
import TimeOptions from "@components/booking/TimeOptions";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||
|
||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||
@@ -150,7 +152,10 @@ const SlotPicker = ({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: browsingDate?.startOf("month"),
|
||||
startTime:
|
||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||
: browsingDate?.startOf("month"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
duration,
|
||||
@@ -216,12 +221,12 @@ function TimezoneDropdown({
|
||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
||||
<p className="flex items-center text-sm font-medium">
|
||||
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
||||
<FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{timeZone}
|
||||
{isTimeOptionsOpen ? (
|
||||
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||
<FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||
) : (
|
||||
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||
<FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||
)}
|
||||
</p>
|
||||
</Popover.Trigger>
|
||||
@@ -253,6 +258,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
useTheme(profile.theme);
|
||||
const { t } = useLocale();
|
||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||
@@ -289,7 +295,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
);
|
||||
}
|
||||
}, [telemetry]);
|
||||
|
||||
const embedUiConfig = useEmbedUiConfig();
|
||||
// get dynamic user list here
|
||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||
|
||||
@@ -302,6 +308,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||
|
||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||
|
||||
// Define conditional gates here
|
||||
const gates = [
|
||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||
@@ -335,7 +343,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
<div>
|
||||
<main
|
||||
className={classNames(
|
||||
"flex-col md:mx-4 lg:flex",
|
||||
"flex flex-col md:mx-4",
|
||||
shouldAlignCentrally ? "items-center" : "items-start",
|
||||
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
||||
)}>
|
||||
@@ -350,54 +358,55 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
isEmbed && "mx-auto"
|
||||
)}>
|
||||
<div className="overflow-hidden md:flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||
)}>
|
||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="flex items-start text-sm font-medium">
|
||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
||||
<div>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline">
|
||||
{t("occurrence", {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
{showEventTypeDetails && (
|
||||
<div
|
||||
className={classNames(
|
||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||
)}>
|
||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="flex items-start text-sm font-medium">
|
||||
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
<div>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline">
|
||||
{t("occurrence", {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{stripeAppData.price > 0 && (
|
||||
<p className="-ml-2 px-2 text-sm font-medium">
|
||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
{timezoneDropdown}
|
||||
</BookingDescription>
|
||||
)}
|
||||
{stripeAppData.price > 0 && (
|
||||
<p className="-ml-2 px-2 text-sm font-medium">
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
{timezoneDropdown}
|
||||
</BookingDescription>
|
||||
|
||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p
|
||||
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
||||
@@ -405,12 +414,13 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)*/}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SlotPicker
|
||||
weekStart={
|
||||
typeof profile.weekStart === "string"
|
||||
@@ -436,7 +446,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
|
||||
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
||||
{!restProps.isBrandingHidden && !isEmbed && <PoweredByCal />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,8 @@ import { useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { ReactMultiEmail } from "react-multi-email";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -26,6 +25,7 @@ import { LocationObject, LocationType } from "@calcom/core/location";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useEmbedUiConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
@@ -37,9 +37,29 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
||||
import { Group, RadioField } from "@calcom/ui";
|
||||
import {
|
||||
AddressInput,
|
||||
Button,
|
||||
EmailField,
|
||||
EmailInput,
|
||||
Form,
|
||||
Group,
|
||||
PhoneInput,
|
||||
RadioField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiUserPlus,
|
||||
FiCalendar,
|
||||
FiX,
|
||||
FiInfo,
|
||||
FiCreditCard,
|
||||
FiRefreshCw,
|
||||
FiUser,
|
||||
FiAlertTriangle,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
@@ -48,7 +68,6 @@ import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import Gates, { Gate, GateState } from "@components/Gates";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
@@ -64,7 +83,7 @@ type BookingFormValues = {
|
||||
email: string;
|
||||
notes?: string;
|
||||
locationType?: EventLocationType["type"];
|
||||
guests?: string[];
|
||||
guests?: { email: string }[];
|
||||
address?: string;
|
||||
attendeeAddress?: string;
|
||||
phone?: string;
|
||||
@@ -89,6 +108,7 @@ const BookingPage = ({
|
||||
const { t, i18n } = useLocale();
|
||||
const { duration: queryDuration } = useRouterQuery("duration");
|
||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||
const embedUiConfig = useEmbedUiConfig();
|
||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||
const router = useRouter();
|
||||
@@ -141,6 +161,7 @@ const BookingPage = ({
|
||||
isSuccessBookingPage: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -156,6 +177,7 @@ const BookingPage = ({
|
||||
allRemainingBookings: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -180,7 +202,9 @@ const BookingPage = ({
|
||||
|
||||
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
|
||||
const guestListEmails = !isDynamicGroupBooking
|
||||
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
||||
? booking?.attendees.slice(1).map((attendee) => {
|
||||
return { email: attendee.email };
|
||||
})
|
||||
: [];
|
||||
|
||||
// There should only exists one default userData variable for primaryAttendee.
|
||||
@@ -199,7 +223,9 @@ const BookingPage = ({
|
||||
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
notes: (router.query.notes as string) || "",
|
||||
guests: ensureArray(router.query.guest) as string[],
|
||||
guests: ensureArray(router.query.guest).map((guest) => {
|
||||
return { email: guest as string };
|
||||
}),
|
||||
customInputs: eventType.customInputs.reduce(
|
||||
(customInputs, input) => ({
|
||||
...customInputs,
|
||||
@@ -241,6 +267,7 @@ const BookingPage = ({
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().trim().email(),
|
||||
guests: z.array(z.object({ email: z.string().email() })).optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
@@ -259,6 +286,10 @@ const BookingPage = ({
|
||||
defaultValues: defaultValues(),
|
||||
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
||||
});
|
||||
const guestsField = useFieldArray({
|
||||
name: "guests",
|
||||
control: bookingForm.control,
|
||||
});
|
||||
|
||||
const selectedLocationType = useWatch({
|
||||
control: bookingForm.control,
|
||||
@@ -297,6 +328,40 @@ const BookingPage = ({
|
||||
}
|
||||
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
bookingForm.clearErrors();
|
||||
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
|
||||
}));
|
||||
|
||||
// Checking if custom inputs of type Phone number are valid to display error message on UI
|
||||
if (eventType.customInputs.length) {
|
||||
let isErrorFound = false;
|
||||
eventType.customInputs.forEach((customInput) => {
|
||||
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
|
||||
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
|
||||
try {
|
||||
z.string({
|
||||
errorMap: () => ({
|
||||
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
|
||||
}),
|
||||
})
|
||||
.refine((val) => isValidPhoneNumber(val), {
|
||||
message: "Phone number is invalid",
|
||||
})
|
||||
.parse(input?.value);
|
||||
} catch (err) {
|
||||
isErrorFound = true;
|
||||
bookingForm.setError(`customInputs.${customInput.id}`, {
|
||||
type: "custom",
|
||||
message: "Invalid Phone number",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (isErrorFound) return;
|
||||
}
|
||||
|
||||
telemetry.event(
|
||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||
{ isTeamBooking: document.URL.includes("team/") }
|
||||
@@ -315,6 +380,45 @@ const BookingPage = ({
|
||||
{}
|
||||
);
|
||||
|
||||
if (eventType.customInputs.length > 0) {
|
||||
// find all required custom inputs and ensure they are filled out in the booking form
|
||||
const requiredCustomInputs = eventType.customInputs.filter((input) => input.required);
|
||||
const missingRequiredCustomInputs = requiredCustomInputs.filter(
|
||||
(input) => !booking?.customInputs?.[input.id]
|
||||
);
|
||||
if (missingRequiredCustomInputs.length > 0) {
|
||||
missingRequiredCustomInputs.forEach((input) => {
|
||||
bookingForm.setError(`customInputs.${input.id}`, {
|
||||
type: "required",
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that guests are unique
|
||||
let alreadyInvited = false;
|
||||
booking.guests?.forEach((guest, index) => {
|
||||
if (guest.email === booking.email) {
|
||||
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
||||
alreadyInvited = true;
|
||||
}
|
||||
|
||||
if (booking.guests) {
|
||||
let guestCount = 0;
|
||||
for (const checkGuest of booking.guests) {
|
||||
if (checkGuest.email === guest.email) guestCount++;
|
||||
if (guestCount > 1) {
|
||||
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
||||
alreadyInvited = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (alreadyInvited) return;
|
||||
|
||||
if (recurringDates.length) {
|
||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||
const recurringEventId = uuidv4();
|
||||
@@ -337,10 +441,7 @@ const BookingPage = ({
|
||||
attendeeAddress: booking.attendeeAddress,
|
||||
}),
|
||||
metadata,
|
||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
||||
})),
|
||||
customInputs: bookingCustomInputs,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
@@ -348,6 +449,7 @@ const BookingPage = ({
|
||||
? booking.phone
|
||||
: booking.smsReminderNumber || undefined,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
guests: booking.guests?.map((guest) => guest.email),
|
||||
}));
|
||||
recurringMutation.mutate(recurringBookings);
|
||||
} else {
|
||||
@@ -368,10 +470,7 @@ const BookingPage = ({
|
||||
attendeeAddress: booking.attendeeAddress,
|
||||
}),
|
||||
metadata,
|
||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
||||
})),
|
||||
customInputs: bookingCustomInputs,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
@@ -379,6 +478,7 @@ const BookingPage = ({
|
||||
? booking.phone
|
||||
: booking.smsReminderNumber || undefined,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
guests: booking.guests?.map((guest) => guest.email),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -406,6 +506,7 @@ const BookingPage = ({
|
||||
}
|
||||
});
|
||||
}
|
||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
|
||||
// Define conditional gates here
|
||||
@@ -431,7 +532,7 @@ const BookingPage = ({
|
||||
})}{" "}
|
||||
| {APP_NAME}
|
||||
</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favico.ico" />
|
||||
</Head>
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||
@@ -439,104 +540,106 @@ const BookingPage = ({
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "mx-auto" : "",
|
||||
isEmbed ? "" : "sm:my-24",
|
||||
"my-0 max-w-3xl "
|
||||
"my-0 max-w-3xl"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"main overflow-hidden",
|
||||
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white",
|
||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
|
||||
"dark:border-darkgray-300 rounded-md sm:border"
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
{stripeAppData.price > 0 && (
|
||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||
<p className="-ml-2 inline-block items-center px-2">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: recurringEventCount,
|
||||
})}
|
||||
{showEventTypeDetails && (
|
||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
{stripeAppData.price > 0 && (
|
||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||
<div className="text-sm font-medium">
|
||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||
{!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||
return <p key={key}>{timeFormatted}</p>;
|
||||
})}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||
<p key={key}>{timeFormatted}</p>
|
||||
))}>
|
||||
<p className="dark:text-darkgray-600 text-sm">
|
||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="line-through ">
|
||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<p className="-ml-2 inline-block items-center px-2">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<Icon.FiUser
|
||||
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
}`}
|
||||
/>
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
} mb-2 font-medium`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<div className="text-sm font-medium">
|
||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||
{!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||
return <p key={key}>{timeFormatted}</p>;
|
||||
})}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||
<p key={key}>{timeFormatted}</p>
|
||||
))}>
|
||||
<p className="dark:text-darkgray-600 text-sm">
|
||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</BookingDescription>
|
||||
</div>
|
||||
<div className="p-6 sm:w-1/2">
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="line-through ">
|
||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<FiUser
|
||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
}`}
|
||||
/>
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
} mb-2 font-medium`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</BookingDescription>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
|
||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
@@ -573,7 +676,7 @@ const BookingPage = ({
|
||||
/>
|
||||
{bookingForm.formState.errors.email && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("email_validation_error")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -614,8 +717,8 @@ const BookingPage = ({
|
||||
value={location.type}
|
||||
defaultChecked={i === 0}
|
||||
/>
|
||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
|
||||
{locationKeyToString(location)}
|
||||
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
|
||||
{t(locationKeyToString(location) ?? "")}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
@@ -639,7 +742,7 @@ const BookingPage = ({
|
||||
{selectedLocationType === LocationType.Phone
|
||||
? t("phone_number")
|
||||
: selectedLocationType === LocationType.AttendeeInPerson
|
||||
? t("Address")
|
||||
? t("address")
|
||||
: ""}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -666,7 +769,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
{bookingForm.formState.errors.phone && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -679,8 +782,11 @@ const BookingPage = ({
|
||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
className={classNames(
|
||||
"mb-1 block text-sm font-medium text-gray-700 transition-colors dark:text-white",
|
||||
bookingForm.formState.errors.customInputs?.[input.id] && "!text-red-700"
|
||||
)}>
|
||||
{input.label} {input.required && <span className="text-red-700">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||
@@ -745,82 +851,51 @@ const BookingPage = ({
|
||||
</div>
|
||||
)}
|
||||
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
||||
<div className="">
|
||||
<div className="flex">
|
||||
<Group
|
||||
onValueChange={(e) => {
|
||||
bookingForm.setValue(`customInputs.${input.id}`, e);
|
||||
}}>
|
||||
<>
|
||||
{input.options.map((option, i) => (
|
||||
<RadioField
|
||||
label={option.label}
|
||||
key={`option.${i}.radio`}
|
||||
value={option.label}
|
||||
id={`option.${i}.radio`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</Group>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Group
|
||||
name={`customInputs.${input.id}`}
|
||||
required={input.required}
|
||||
onValueChange={(e) => {
|
||||
bookingForm.setValue(`customInputs.${input.id}`, e);
|
||||
}}>
|
||||
<>
|
||||
{input.options.map((option, i) => (
|
||||
<RadioField
|
||||
label={option.label}
|
||||
key={`option.${input.id}.${i}.radio`}
|
||||
value={option.label}
|
||||
id={`option.${input.id}.${i}.radio`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
{bookingForm.formState.errors.customInputs?.[input.id] && (
|
||||
<div className="mt-px flex items-center text-xs text-red-700 ">
|
||||
<p>{t("required")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.PHONE && (
|
||||
<div>
|
||||
<PhoneInput<BookingFormValues>
|
||||
name={`customInputs.${input.id}`}
|
||||
control={bookingForm.control}
|
||||
placeholder={t("enter_phone_number")}
|
||||
id={`customInputs.${input.id}`}
|
||||
required={input.required}
|
||||
/>
|
||||
{bookingForm.formState.errors?.customInputs?.[input.id] && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!eventType.disableGuests && guestToggle && (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
{!disableInput && (
|
||||
<Controller
|
||||
control={bookingForm.control}
|
||||
name="guests"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder={<span className="dark:text-darkgray-600">guest@example.com</span>}
|
||||
emails={value}
|
||||
onChange={onChange}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index} className="cursor-pointer">
|
||||
{email}
|
||||
{!disableInput && (
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Custom code when guest emails should not be editable */}
|
||||
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
||||
<div data-tag className="react-multi-email">
|
||||
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
||||
{guestListEmails.map((email, index) => {
|
||||
return (
|
||||
<div key={index} className="cursor-pointer">
|
||||
<span data-tag>{email}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
@@ -839,7 +914,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
{bookingForm.formState.errors.smsReminderNumber && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -873,25 +948,85 @@ const BookingPage = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!eventType.disableGuests && guestsField.fields.length ? (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
<ul>
|
||||
{guestsField.fields.map((field, index) => (
|
||||
<li key={field.id}>
|
||||
<EmailField
|
||||
{...bookingForm.register(`guests.${index}.email` as const)}
|
||||
className={classNames(
|
||||
inputClassName,
|
||||
bookingForm.formState.errors.guests?.[index] &&
|
||||
"!focus:ring-red-700 !border-red-700",
|
||||
"border-r-0"
|
||||
)}
|
||||
addOnClassname={classNames(
|
||||
"border-gray-300 border block border-l-0 disabled:bg-gray-200 disabled:hover:cursor-not-allowed bg-transparent disabled:text-gray-500 dark:border-darkgray-300 ",
|
||||
bookingForm.formState.errors.guests?.[index] &&
|
||||
"!focus:ring-red-700 !border-red-700"
|
||||
)}
|
||||
placeholder="guest@example.com"
|
||||
label={<></>}
|
||||
required
|
||||
addOnSuffix={
|
||||
<Tooltip content="Remove guest">
|
||||
<button
|
||||
className="m-1 disabled:hover:cursor-not-allowed"
|
||||
type="button"
|
||||
onClick={() => guestsField.remove(index)}>
|
||||
<FiX className="text-gray-600" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
{bookingForm.formState.errors.guests?.[index] && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p className="text-red-700">
|
||||
{bookingForm.formState.errors.guests?.[index]?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
StartIcon={FiUserPlus}
|
||||
className="my-2.5"
|
||||
// className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"
|
||||
onClick={() => guestsField.append({ email: "" })}>
|
||||
{t("add_another")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
{!eventType.disableGuests && !guestToggle && (
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
tooltip={t("additional_guests")}
|
||||
StartIcon={Icon.FiUserPlus}
|
||||
onClick={() => setGuestToggle(!guestToggle)}
|
||||
className="mr-auto"
|
||||
/>
|
||||
)}
|
||||
{!eventType.disableGuests && !guestsField.fields.length && (
|
||||
<Button
|
||||
color="minimal"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
// We override this for this component only for now - as we don't support darkmode everywhere in the app
|
||||
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
|
||||
variant="button"
|
||||
StartIcon={FiUserPlus}
|
||||
onClick={() => {
|
||||
guestsField.append({ email: "" });
|
||||
}}
|
||||
className="mr-auto">
|
||||
{t("additional_guests")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<Button color="minimal" type="button" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -923,7 +1058,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Icon.FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { components } from "react-select";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@@ -14,32 +15,26 @@ import {
|
||||
LocationObject,
|
||||
LocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import Select from "@components/ui/form/Select";
|
||||
import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: EventLocationType["type"];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
interface ISetLocationDialog {
|
||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||
selection?: OptionTypeBase;
|
||||
selection?: LocationOption;
|
||||
booking?: BookingItem;
|
||||
defaultValues?: LocationObject[];
|
||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOpenDialog: boolean;
|
||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
||||
setSelectedLocation?: (param: LocationOption | undefined) => void;
|
||||
setEditingLocationType?: (param: string) => void;
|
||||
}
|
||||
|
||||
@@ -211,11 +206,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
})();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog}>
|
||||
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
<FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
@@ -223,7 +218,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
{t("edit_location")}
|
||||
</h3>
|
||||
{!booking && (
|
||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
||||
Can't find the right video app? Visit our
|
||||
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
||||
App Store
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
||||
@@ -296,57 +299,31 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
name="locationType"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<Select<{ label: string; value: string; icon?: string }>
|
||||
maxMenuHeight={300}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={locationOptions}
|
||||
components={{
|
||||
Option: (props) => (
|
||||
<components.Option {...props}>
|
||||
<div className="flex items-center gap-3">
|
||||
{props.data.icon && (
|
||||
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span
|
||||
className={classNames(
|
||||
"text-sm font-medium",
|
||||
props.isSelected ? "text-white" : "text-gray-900"
|
||||
)}>
|
||||
{props.data.label}
|
||||
</span>
|
||||
</div>
|
||||
</components.Option>
|
||||
),
|
||||
}}
|
||||
formatOptionLabel={(e) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
|
||||
<span>{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
formatGroupLabel={(e) => (
|
||||
<p className="text-xs font-medium text-gray-600">{e.label}</p>
|
||||
)}
|
||||
isSearchable
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="py-4">
|
||||
<LocationSelect
|
||||
maxMenuHeight={300}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={locationOptions}
|
||||
isSearchable
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -354,13 +331,13 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<DialogFooter>
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
setEditingLocationType?.("");
|
||||
locationFormMethods.unregister("locationType");
|
||||
locationFormMethods.unregister(["locationType", "locationLink"]);
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
showToast,
|
||||
TextArea,
|
||||
} from "@calcom/ui";
|
||||
import { FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -43,7 +43,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiClock className="m-auto h-6 w-6" />
|
||||
<FiClock className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -109,7 +110,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="min-w-4 mb-2">
|
||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
||||
{t("availability")}
|
||||
</label>
|
||||
</div>
|
||||
@@ -149,7 +150,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ml-4">-</span>
|
||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -164,13 +165,13 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<Icon.FiGlobe className="mr-2" />
|
||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${schedule?.schedule.id}`}
|
||||
color="minimal"
|
||||
EndIcon={Icon.FiExternalLink}
|
||||
EndIcon={FiExternalLink}
|
||||
target="_blank"
|
||||
className="justify-center border sm:border-0"
|
||||
rel="noopener noreferrer">
|
||||
|
||||
@@ -5,7 +5,8 @@ import { FC } from "react";
|
||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
||||
import { Button, Label, Select, TextField } from "@calcom/ui";
|
||||
import { FiPlus, FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
interface OptionTypeBase {
|
||||
label: string;
|
||||
@@ -40,6 +41,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
value: EventTypeCustomInputType.RADIO,
|
||||
label: t("radio"),
|
||||
},
|
||||
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
||||
];
|
||||
|
||||
const { selectedCustomInput } = props;
|
||||
@@ -76,7 +78,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
defaultValue={selectedInputOption}
|
||||
options={inputOptions}
|
||||
isSearchable={false}
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
||||
onChange={(option) => option && field.onChange(option.value)}
|
||||
value={selectedInputOption}
|
||||
onBlur={field.onBlur}
|
||||
@@ -133,8 +135,8 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
value={selectedCustomInput?.id || -1}
|
||||
{...register("id", { valueAsNumber: true })}
|
||||
/>
|
||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-4">
|
||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
||||
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
|
||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -182,9 +184,9 @@ function RadioInputHandler({
|
||||
{...register(`options.${index}.label` as const, { required: true })}
|
||||
addOnSuffix={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiX}
|
||||
StartIcon={FiX}
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
@@ -195,7 +197,7 @@ function RadioInputHandler({
|
||||
))}
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
className="!text-sm !font-medium"
|
||||
onClick={() => {
|
||||
append({ label: "", type: "text" });
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import type { CustomInputParsed, 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 DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
CustomInputItem,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Icon,
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { FiEdit, FiCopy, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||
|
||||
@@ -36,7 +37,19 @@ const generateHashedLink = (id: number) => {
|
||||
return uid;
|
||||
};
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
|
||||
const getRandomId = (length = 8) => {
|
||||
return (
|
||||
-1 *
|
||||
parseInt(
|
||||
Math.ceil(Math.random() * Date.now())
|
||||
.toPrecision(length)
|
||||
.toString()
|
||||
.replace(".", "")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
@@ -51,7 +64,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
|
||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
@@ -64,6 +76,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
}, [eventType.users, hashedUrl, team?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.customInputs) {
|
||||
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
|
||||
}
|
||||
}, [eventType.customInputs]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-8">
|
||||
{/**
|
||||
@@ -75,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<Label>{t("add_to_calendar")}</Label>
|
||||
<Link href="/apps/categories/calendar">
|
||||
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900">
|
||||
{t("add_another_calendar")}
|
||||
</a>
|
||||
<Link
|
||||
href="/apps/categories/calendar"
|
||||
target="_blank"
|
||||
className="text-sm text-gray-600 hover:text-gray-900">
|
||||
{t("add_another_calendar")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="-mt-1 w-full">
|
||||
@@ -109,8 +128,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
addOnSuffix={
|
||||
<Button
|
||||
type="button"
|
||||
StartIcon={Icon.FiEdit}
|
||||
size="icon"
|
||||
StartIcon={FiEdit}
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}
|
||||
@@ -149,7 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
</ul>
|
||||
{customInputs.length > 0 && (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
color="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -239,13 +258,28 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
defaultValue={eventType.successRedirectUrl || ""}
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
/>
|
||||
<div className="mt-2 flex">
|
||||
<Checkbox
|
||||
description={t("disable_success_page")}
|
||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
||||
{...formMethods.register("metadata.disableSuccessPage")}
|
||||
/>
|
||||
{/*TODO: Extract it out into a component when used more than once*/}
|
||||
{!IS_SELF_HOSTED && (
|
||||
<Link href="https://cal.com/pricing" target="_blank">
|
||||
<Badge variant="orange" className="ml-2">
|
||||
Platform Only
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<SettingsToggle
|
||||
data-testid="hashedLinkCheck"
|
||||
title={t("private_link")}
|
||||
@@ -280,7 +314,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
}}
|
||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||
type="button">
|
||||
<Icon.FiCopy />
|
||||
<FiCopy />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -374,25 +408,26 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
<Controller
|
||||
name="customInputs"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.customInputs.sort((a, b) => a.id - b.id) || []}
|
||||
defaultValue={customInputs}
|
||||
render={() => (
|
||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
Icon={Icon.FiPlus}
|
||||
Icon={FiPlus}
|
||||
title={t("add_new_custom_input_field")}
|
||||
description={t("this_input_will_shown_booking_this_event")}>
|
||||
<CustomInputTypeForm
|
||||
selectedCustomInput={selectedCustomInput}
|
||||
onSubmit={(values) => {
|
||||
const customInput: CustomInputParsed = {
|
||||
id: -1,
|
||||
id: getRandomId(),
|
||||
eventTypeId: -1,
|
||||
label: values.label,
|
||||
placeholder: values.placeholder,
|
||||
required: values.required,
|
||||
type: values.type,
|
||||
options: values.options,
|
||||
hasToBeCreated: true,
|
||||
};
|
||||
if (selectedCustomInput) {
|
||||
selectedCustomInput.label = customInput.label;
|
||||
@@ -400,6 +435,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
selectedCustomInput.required = customInput.required;
|
||||
selectedCustomInput.type = customInput.type;
|
||||
selectedCustomInput.options = customInput.options || undefined;
|
||||
selectedCustomInput.hasToBeCreated = false;
|
||||
// Update by id
|
||||
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
||||
customInputs[inputIndex] = selectedCustomInput;
|
||||
|
||||
@@ -1,43 +1,18 @@
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
|
||||
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen } from "@calcom/ui";
|
||||
import { FiGrid } from "@calcom/ui/components/icon";
|
||||
|
||||
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
|
||||
function AppCardWrapper({
|
||||
app,
|
||||
eventType,
|
||||
getAppData,
|
||||
setAppData,
|
||||
}: {
|
||||
app: RouterOutputs["viewer"]["apps"][number];
|
||||
eventType: EventType;
|
||||
getAppData: GetAppData;
|
||||
setAppData: SetAppData;
|
||||
}) {
|
||||
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
|
||||
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
|
||||
|
||||
if (!Component) {
|
||||
throw new Error('No component found for "' + dirName + '"');
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
|
||||
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
|
||||
<Component key={app.slug} app={app} eventType={eventType} />
|
||||
</EventTypeAppContext.Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const { t } = useLocale();
|
||||
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
|
||||
@@ -86,7 +61,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
<div className="before:border-0">
|
||||
{!isLoading && !installedApps?.length ? (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiGrid}
|
||||
Icon={FiGrid}
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
@@ -97,7 +72,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
/>
|
||||
) : null}
|
||||
{installedApps?.map((app) => (
|
||||
<AppCardWrapper
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
key={app.slug}
|
||||
@@ -113,7 +88,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
) : null}
|
||||
<div className="before:border-0">
|
||||
{notInstalledApps?.map((app) => (
|
||||
<AppCardWrapper
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
key={app.slug}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -10,15 +10,98 @@ import findDurationType from "@calcom/lib/findDurationType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PeriodType } from "@calcom/prisma/client";
|
||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<UseFormRegisterReturn<"minimumBookingNotice">, "ref">
|
||||
>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
|
||||
const { t } = useLocale();
|
||||
const { setValue, getValues } = useFormContext<FormValues>();
|
||||
const durationTypeOptions: {
|
||||
value: DurationType;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
label: t("minutes"),
|
||||
value: "minutes",
|
||||
},
|
||||
{
|
||||
label: t("hours"),
|
||||
value: "hours",
|
||||
},
|
||||
{
|
||||
label: t("days"),
|
||||
value: "days",
|
||||
},
|
||||
];
|
||||
|
||||
const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
|
||||
type: DurationType;
|
||||
value: number;
|
||||
}>({
|
||||
type: findDurationType(getValues(passThroughProps.name)),
|
||||
value: convertToNewDurationType(
|
||||
"minutes",
|
||||
findDurationType(getValues(passThroughProps.name)),
|
||||
getValues(passThroughProps.name)
|
||||
),
|
||||
});
|
||||
// keep hidden field in sync with minimumBookingNoticeDisplayValues
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
passThroughProps.name,
|
||||
convertToNewDurationType(
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-end">
|
||||
<div className="w-1/2 md:w-full">
|
||||
<InputField
|
||||
required
|
||||
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||
onChange={(e) =>
|
||||
setMinimumBookingNoticeDisplayValues({
|
||||
...minimumBookingNoticeDisplayValues,
|
||||
value: parseInt(e.target.value || "0", 10),
|
||||
})
|
||||
}
|
||||
label={t("minimum_booking_notice")}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||
</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||
defaultValue={durationTypeOptions.find(
|
||||
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||
)}
|
||||
onChange={(input) => {
|
||||
if (input) {
|
||||
setMinimumBookingNoticeDisplayValues({
|
||||
...minimumBookingNoticeDisplayValues,
|
||||
type: input.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
options={durationTypeOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
|
||||
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
|
||||
|
||||
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
|
||||
|
||||
const PERIOD_TYPES = [
|
||||
{
|
||||
@@ -49,212 +132,115 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
defaultValue: periodType?.type,
|
||||
});
|
||||
|
||||
const onMinimumNoticeDurationTypeChange = useMemo(
|
||||
() => (durationType?: DurationType) => {
|
||||
if (typeof durationType === "undefined") return;
|
||||
|
||||
// Store current selected type in ref to use in previous run for comparrison.
|
||||
minimumBookingNoticeType.current = durationType;
|
||||
|
||||
// Uses the previous selected type and converts the value to the new type.
|
||||
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
|
||||
const minimumBookingNoticeInDurationType = convertToNewDurationType(
|
||||
prevBookingNoticeType.current,
|
||||
durationType,
|
||||
Number(minimumBookingNoticeInDurationTypeFormValue)
|
||||
);
|
||||
|
||||
// Uses the round up value in the new type to calculate the value in minutes.
|
||||
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
|
||||
// would break in the case that the user enters 90 minutes, changes the type to
|
||||
// hours (that will show 2 hours), but the value in minutes would then remain 90,
|
||||
// which is something else than the user things they save.
|
||||
const minimumBookingNoticeInMinutes = convertToNewDurationType(
|
||||
durationType,
|
||||
"minutes",
|
||||
Number(minimumBookingNoticeInDurationType)
|
||||
);
|
||||
|
||||
// Updates both form values as well as stores the current type as previous type in ref.
|
||||
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
|
||||
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
|
||||
prevBookingNoticeType.current = durationType;
|
||||
},
|
||||
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
|
||||
);
|
||||
|
||||
/**
|
||||
* When the user inputs a new value for minimumBookingNoticeInDurationType,
|
||||
* we calculate the value in minutes, and update this hidden field as well.
|
||||
*/
|
||||
const onMinimumNoticeChange = useMemo(
|
||||
() => (duration: string) => {
|
||||
const minimumBookingNoticeInMinutes = convertToNewDurationType(
|
||||
minimumBookingNoticeType.current,
|
||||
"minutes",
|
||||
Number(duration)
|
||||
);
|
||||
|
||||
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
|
||||
},
|
||||
[formMethods]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
||||
<Controller
|
||||
name="beforeBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.beforeEventBuffer || 0}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const beforeBufferOptions = [
|
||||
{
|
||||
label: t("event_buffer_default"),
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
defaultValue={
|
||||
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
||||
}
|
||||
options={beforeBufferOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
||||
<Controller
|
||||
name="afterBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.afterEventBuffer || 0}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const afterBufferOptions = [
|
||||
{
|
||||
label: t("event_buffer_default"),
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
defaultValue={
|
||||
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
||||
}
|
||||
options={afterBufferOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="flex w-full items-end justify-end">
|
||||
<Controller
|
||||
name="minimumBookingNotice"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const durationTypeOptions = [
|
||||
{
|
||||
label: t("minutes"),
|
||||
value: "minutes",
|
||||
},
|
||||
{
|
||||
label: t("hours"),
|
||||
value: "hours",
|
||||
},
|
||||
{
|
||||
label: t("days"),
|
||||
value: "days",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-1/2 md:w-3/4">
|
||||
<InputField
|
||||
required
|
||||
label={t("minimum_booking_notice")}
|
||||
type="number"
|
||||
placeholder="120"
|
||||
className="mr-2 mb-0 h-[38px] rounded-[4px]"
|
||||
{...formMethods.register("minimumBookingNoticeInDurationType", {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onMinimumNoticeChange(event.target.value),
|
||||
})}
|
||||
/>
|
||||
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4 lg:space-y-8">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
||||
<Controller
|
||||
name="beforeBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.beforeEventBuffer || 0}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const beforeBufferOptions = [
|
||||
{
|
||||
label: t("event_buffer_default"),
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||
defaultValue={durationTypeOptions.find(
|
||||
(option) => option.value === minimumBookingNoticeType.current
|
||||
)}
|
||||
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)}
|
||||
options={durationTypeOptions}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
defaultValue={
|
||||
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
||||
}
|
||||
options={beforeBufferOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
||||
<Controller
|
||||
name="afterBufferTime"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.afterEventBuffer || 0}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const afterBufferOptions = [
|
||||
{
|
||||
label: t("event_buffer_default"),
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
defaultValue={
|
||||
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
||||
}
|
||||
options={afterBufferOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const slotIntervalOptions = [
|
||||
{
|
||||
label: t("slot_interval_default"),
|
||||
value: -1,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
|
||||
slotIntervalOptions[0]
|
||||
}
|
||||
options={slotIntervalOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const slotIntervalOptions = [
|
||||
{
|
||||
label: t("slot_interval_default"),
|
||||
value: -1,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
|
||||
slotIntervalOptions[0]
|
||||
}
|
||||
options={slotIntervalOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<hr />
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
control={formMethods.control}
|
||||
@@ -276,8 +262,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
<hr />
|
||||
<Controller
|
||||
name="periodType"
|
||||
control={formMethods.control}
|
||||
@@ -327,7 +312,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
</div>
|
||||
)}
|
||||
{period.type === "RANGE" && (
|
||||
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
||||
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
control={formMethods.control}
|
||||
@@ -347,7 +332,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span> : null}
|
||||
{period.suffix ? (
|
||||
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2"> {period.suffix}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -410,7 +397,9 @@ const BookingLimits = () => {
|
||||
.map(([key, bookingAmount]) => {
|
||||
const bookingLimitKey = key as BookingLimitsKey;
|
||||
return (
|
||||
<div className="mb-2 flex items-center space-x-2 text-sm" key={bookingLimitKey}>
|
||||
<div
|
||||
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
|
||||
key={bookingLimitKey}>
|
||||
<Input
|
||||
id={`${bookingLimitKey}-limit`}
|
||||
type="number"
|
||||
@@ -431,19 +420,21 @@ const BookingLimits = () => {
|
||||
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
||||
onChange={(val) => {
|
||||
const current = currentBookingLimits;
|
||||
const currentValue = watchBookingLimits[bookingLimitKey];
|
||||
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[bookingLimitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[val?.value as BookingLimitsKey]: watchBookingLimits[bookingLimitKey],
|
||||
[val?.value as BookingLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
StartIcon={Icon.FiTrash}
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const current = currentBookingLimits;
|
||||
@@ -457,7 +448,7 @@ const BookingLimits = () => {
|
||||
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => {
|
||||
if (!currentBookingLimits || !watchBookingLimits) return;
|
||||
const currentKeys = Object.keys(watchBookingLimits);
|
||||
@@ -470,7 +461,7 @@ const BookingLimits = () => {
|
||||
|
||||
setValue("bookingLimits", {
|
||||
...watchBookingLimits,
|
||||
[rest[0].value]: undefined,
|
||||
[rest[0].value]: 1,
|
||||
});
|
||||
}}>
|
||||
{t("add_limit")}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { EventTypeSetupInfered } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
|
||||
import RecurringEventController from "./RecurringEventController";
|
||||
|
||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const stripeAppData = getStripeAppData(eventType);
|
||||
|
||||
const requirePayment = stripeAppData.price > 0;
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { MultiValue } from "react-select";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
import { slugify } from "@lib/slugify";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import LocationSelect, {
|
||||
SingleValueLocationOption,
|
||||
LocationOption,
|
||||
} from "@components/ui/form/LocationSelect";
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: EventLocationType["type"];
|
||||
disabled?: boolean;
|
||||
const getLocationFromType = (
|
||||
type: EventLocationType["type"],
|
||||
locationOptions: Pick<EventTypeSetupProps, "locationOptions">["locationOptions"]
|
||||
) => {
|
||||
for (const locationOption of locationOptions) {
|
||||
const option = locationOption.options.find((option) => option.value === type);
|
||||
if (option) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const EventSetupTab = (
|
||||
props: Pick<EventTypeSetupInfered, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||
) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { eventType, locationOptions, team } = props;
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||
|
||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||
@@ -49,7 +61,8 @@ export const EventSetupTab = (
|
||||
);
|
||||
|
||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
||||
setSelectedLocation(locationOptions.find((option) => option.value === type));
|
||||
const option = getLocationFromType(type, locationOptions);
|
||||
setSelectedLocation(option);
|
||||
setShowLocationModal(true);
|
||||
};
|
||||
|
||||
@@ -125,18 +138,21 @@ export const EventSetupTab = (
|
||||
return true;
|
||||
});
|
||||
|
||||
const defaultValue = locationOptions.find((item) => item.label === "video")?.options;
|
||||
return (
|
||||
<div className="w-full">
|
||||
{validLocations.length === 0 && (
|
||||
<div className="flex">
|
||||
<Select
|
||||
<LocationSelect
|
||||
defaultValue={defaultValue}
|
||||
placeholder={t("select")}
|
||||
options={locationOptions}
|
||||
isSearchable={false}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
onChange={(e) => {
|
||||
menuPlacement="auto"
|
||||
onChange={(e: SingleValueLocationOption) => {
|
||||
if (e?.value) {
|
||||
const newLocationType: EventLocationType["type"] = e.value;
|
||||
const newLocationType = e.value;
|
||||
const eventLocationType = getEventLocationType(newLocationType);
|
||||
if (!eventLocationType) {
|
||||
return;
|
||||
@@ -160,7 +176,7 @@ export const EventSetupTab = (
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
|
||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
<div className="flex max-w-full justify-between">
|
||||
<div key={index} className="flex flex-grow items-center">
|
||||
<img
|
||||
@@ -169,7 +185,7 @@ export const EventSetupTab = (
|
||||
alt={`${eventLocationType.label} logo`}
|
||||
/>
|
||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
||||
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
||||
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
@@ -185,19 +201,36 @@ export const EventSetupTab = (
|
||||
}}
|
||||
aria-label={t("edit")}
|
||||
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
||||
<Icon.FiEdit2 className="h-4 w-4" />
|
||||
<FiEdit2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
||||
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{validLocations.some((location) => location.type === MeetLocationType) && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||
<Trans i18nKey="event_type_requres_google_cal">
|
||||
<p>
|
||||
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`}
|
||||
className="underline">
|
||||
here.
|
||||
</Link>{" "}
|
||||
We will fall back to Cal video if you do not change it.
|
||||
</p>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||
<li>
|
||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
<Button StartIcon={FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
{t("add_location")}
|
||||
</Button>
|
||||
</li>
|
||||
@@ -262,12 +295,14 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
}}
|
||||
@@ -288,7 +323,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
formMethods.setValue("length", Number(option?.value));
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -296,14 +331,11 @@ export const EventSetupTab = (
|
||||
) : (
|
||||
<TextField
|
||||
required
|
||||
name="length"
|
||||
type="number"
|
||||
label={t("duration")}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
defaultValue={eventType.length ?? 15}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("length", Number(e.target.value));
|
||||
}}
|
||||
{...formMethods.register("length")}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
/>
|
||||
)}
|
||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||
@@ -327,6 +359,7 @@ export const EventSetupTab = (
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
{t("location")}
|
||||
</Skeleton>
|
||||
|
||||
<Controller
|
||||
name="locations"
|
||||
control={formMethods.control}
|
||||
@@ -343,7 +376,9 @@ export const EventSetupTab = (
|
||||
saveLocation={saveLocation}
|
||||
defaultValues={formMethods.getValues("locations")}
|
||||
selection={
|
||||
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
|
||||
selectedLocation
|
||||
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
|
||||
: undefined
|
||||
}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
setEditingLocationType={setEditingLocationType}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { SchedulingType } from "@prisma/client/";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { ComponentProps } from "react";
|
||||
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
||||
import { Options } from "react-select";
|
||||
|
||||
import CheckedTeamSelect, {
|
||||
CheckedSelectOption,
|
||||
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
|
||||
interface IMemberToValue {
|
||||
id: number | null;
|
||||
@@ -21,15 +25,66 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
||||
email,
|
||||
});
|
||||
|
||||
export const EventTeamTab = ({
|
||||
eventType,
|
||||
team,
|
||||
teamMembers,
|
||||
currentUserMembership,
|
||||
}: Pick<EventTypeSetupInfered, "eventType" | "teamMembers" | "team" | "currentUserMembership">) => {
|
||||
const FixedHosts = ({
|
||||
control,
|
||||
labelText,
|
||||
placeholder,
|
||||
options = [],
|
||||
...rest
|
||||
}: {
|
||||
control: Control<FormValues>;
|
||||
labelText: string;
|
||||
placeholder: string;
|
||||
options?: Options<CheckedSelectOption>;
|
||||
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{labelText}</Label>
|
||||
<Controller
|
||||
name="hostsFixed"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) => {
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
const schedulingType = useWatch({
|
||||
control: formMethods.control,
|
||||
name: "schedulingType",
|
||||
});
|
||||
|
||||
const schedulingTypeOptions: {
|
||||
value: SchedulingType;
|
||||
label: string;
|
||||
@@ -47,14 +102,13 @@ export const EventTeamTab = ({
|
||||
},
|
||||
];
|
||||
|
||||
const teamMembersToValues = useMemo(() => {
|
||||
return teamMembers.map(mapUserToValue);
|
||||
}, [teamMembers]);
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col pb-8">
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
<Controller
|
||||
name="schedulingType"
|
||||
@@ -72,75 +126,75 @@ export const EventTeamTab = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("team")}</Label>
|
||||
<Controller
|
||||
name="users"
|
||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.users.map((user) => user.id.toString())}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) => onChange(options.map((user) => user.value))}
|
||||
value={value
|
||||
.map(
|
||||
(userId) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
teamMembers.map(mapUserToValue).find((member) => member.value === userId)!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembersToValues}
|
||||
placeholder={t("add_attendees")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamProfileCard = ({
|
||||
id,
|
||||
name,
|
||||
hasPermsToDelete,
|
||||
email,
|
||||
username,
|
||||
}: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
username: string | null;
|
||||
hasPermsToDelete: boolean;
|
||||
}) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const avatar = `${WEBAPP_URL}/${username}/avatar.png`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center border border-gray-300 px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md">
|
||||
<Avatar imageSrc={avatar} gravatarFallbackMd5="ajsdkwakhsd1231" size="md" alt="Team Image" />
|
||||
<div className="flex flex-col pl-3">
|
||||
<div className="">
|
||||
<span className="pr-2 text-sm font-semibold leading-none text-black">{name}</span>
|
||||
</div>
|
||||
<p className="text-sm font-normal leading-normal text-gray-700">{email}</p>
|
||||
</div>
|
||||
{hasPermsToDelete && (
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
StartIcon={Icon.FiTrash}
|
||||
onClick={() => {
|
||||
formMethods.setValue(
|
||||
"users",
|
||||
formMethods.getValues("users").filter((user) => user !== id.toString())
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||
<>
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{t("round_robin_hosts")}</Label>
|
||||
<Controller
|
||||
name="hosts"
|
||||
control={formMethods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) =>
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: false,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
)
|
||||
}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
teamMembers
|
||||
.map(mapUserToValue)
|
||||
.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembers.map(mapUserToValue)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods
|
||||
.getValues("hostsFixed")
|
||||
.find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_attendees")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { TbWebhook } from "react-icons/tb";
|
||||
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
export const EventTeamWebhooksTab = ({
|
||||
eventType,
|
||||
team,
|
||||
}: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||
|
||||
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
variant: "other",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
|
||||
|
||||
const subscriberUrlReserved = (subscriberUrl: string, id?: string): boolean => {
|
||||
return !!webhooks?.find(
|
||||
(webhook) => webhook.subscriberUrl === subscriberUrl && (!id || webhook.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess() {
|
||||
setEditModalOpen(false);
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
showToast(t("webhook_updated_successfully"), "success");
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("webhook_created_successfully"), "success");
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
setCreateModalOpen(false);
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
|
||||
if (subscriberUrlReserved(values.subscriberUrl, values.id)) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
createWebhookMutation.mutate({
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: eventType.id,
|
||||
});
|
||||
};
|
||||
|
||||
const NewWebhookButton = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{team && webhooks && !isLoading && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="mt-4 mb-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<NewWebhookButton />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={TbWebhook}
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||
buttonRaw={<NewWebhookButton />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New webhook dialog */}
|
||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
title={t("create_webhook")}
|
||||
description={t("create_webhook_team_event_type")}>
|
||||
<WebhookForm
|
||||
onSubmit={onCreateWebhook}
|
||||
onCancel={() => setCreateModalOpen(false)}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Edit webhook dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent title={t("edit_webhook")}>
|
||||
<WebhookForm
|
||||
webhook={webhookToEdit}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
onCancel={() => setEditModalOpen(false)}
|
||||
onSubmit={(values: WebhookFormSubmitData) => {
|
||||
if (subscriberUrlReserved(values.subscriberUrl, webhookToEdit?.id || "")) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.changeSecret) {
|
||||
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
editWebhookMutation.mutate({
|
||||
id: webhookToEdit?.id || "",
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: webhookToEdit?.eventTypeId || undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Loader } from "react-feather";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { TbWebhook } from "react-icons/tb";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -15,15 +16,14 @@ import {
|
||||
ButtonGroup,
|
||||
ConfirmationDialogContent,
|
||||
Dialog,
|
||||
Divider,
|
||||
DropdownMenuSeparator,
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
DropdownMenuTrigger,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
Label,
|
||||
Shell,
|
||||
showToast,
|
||||
Skeleton,
|
||||
Switch,
|
||||
@@ -31,15 +31,29 @@ import {
|
||||
VerticalDivider,
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiLink,
|
||||
FiCalendar,
|
||||
FiClock,
|
||||
FiSliders,
|
||||
FiRepeat,
|
||||
FiGrid,
|
||||
FiZap,
|
||||
FiUsers,
|
||||
FiExternalLink,
|
||||
FiCode,
|
||||
FiTrash,
|
||||
FiMoreHorizontal,
|
||||
FiLoader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
eventType: EventTypeSetupInfered["eventType"];
|
||||
currentUserMembership: EventTypeSetupInfered["currentUserMembership"];
|
||||
team: EventTypeSetupInfered["team"];
|
||||
eventType: EventTypeSetupProps["eventType"];
|
||||
currentUserMembership: EventTypeSetupProps["currentUserMembership"];
|
||||
team: EventTypeSetupProps["team"];
|
||||
disableBorder?: boolean;
|
||||
enabledAppsNumber: number;
|
||||
installedAppsNumber: number;
|
||||
@@ -56,48 +70,51 @@ function getNavigation(props: {
|
||||
installedAppsNumber: number;
|
||||
}) {
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||
const duration =
|
||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "event_setup_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||
icon: Icon.FiLink,
|
||||
info: `${eventType.length} Mins`, // TODO: Get this from props
|
||||
icon: FiLink,
|
||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "availability",
|
||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||
icon: Icon.FiCalendar,
|
||||
icon: FiCalendar,
|
||||
info: `default_schedule_name`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "event_limit_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||
icon: Icon.FiClock,
|
||||
icon: FiClock,
|
||||
info: `event_limit_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "event_advanced_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||
icon: Icon.FiSliders,
|
||||
icon: FiSliders,
|
||||
info: `event_advanced_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||
icon: Icon.FiRepeat,
|
||||
icon: FiRepeat,
|
||||
info: `recurring_event_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||
icon: Icon.FiGrid,
|
||||
icon: FiGrid,
|
||||
//TODO: Handle proper translation with count handling
|
||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||
icon: Icon.FiZap,
|
||||
icon: FiZap,
|
||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||
},
|
||||
];
|
||||
@@ -150,13 +167,20 @@ function EventTypeSingleLayout({
|
||||
enabledWorkflowsNumber,
|
||||
});
|
||||
// If there is a team put this navigation item within the tabs
|
||||
if (team)
|
||||
if (team) {
|
||||
navigation.splice(2, 0, {
|
||||
name: "scheduling_type",
|
||||
name: "assignment",
|
||||
href: `/event-types/${eventType.id}?tabName=team`,
|
||||
icon: Icon.FiUsers,
|
||||
icon: FiUsers,
|
||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||
});
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||
icon: TbWebhook,
|
||||
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
return navigation;
|
||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
||||
|
||||
@@ -198,17 +222,17 @@ function EventTypeSingleLayout({
|
||||
<Button
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
size="icon"
|
||||
variant="icon"
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
StartIcon={FiExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiLink}
|
||||
variant="icon"
|
||||
StartIcon={FiLink}
|
||||
tooltip={t("copy_link")}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
@@ -217,15 +241,15 @@ function EventTypeSingleLayout({
|
||||
/>
|
||||
<EmbedButton
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
StartIcon={Icon.FiCode}
|
||||
StartIcon={FiCode}
|
||||
color="secondary"
|
||||
size="icon"
|
||||
variant="icon"
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiTrash}
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
tooltip={t("delete")}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
@@ -235,37 +259,41 @@ function EventTypeSingleLayout({
|
||||
<VerticalDivider className="hidden lg:block" />
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger className="block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 lg:hidden">
|
||||
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" />
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
<DropdownItem
|
||||
target="_blank"
|
||||
type="button"
|
||||
StartIcon={FiExternalLink}
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
className="min-w-full">
|
||||
rel="noreferrer">
|
||||
{t("preview")}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<Button color="minimal" StartIcon={Icon.FiLink}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={FiLink}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
}}>
|
||||
{t("copy_link")}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiTrash}
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={FiTrash}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
className="min-w-full">
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<Divider />
|
||||
<DropdownMenuSeparator className="block sm:hidden" />
|
||||
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
@@ -294,8 +322,8 @@ function EventTypeSingleLayout({
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||
<Suspense fallback={<FiLoader />}>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
className="primary-navigation"
|
||||
@@ -310,14 +338,14 @@ function EventTypeSingleLayout({
|
||||
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 rounded-md border-neutral-200 bg-white sm:mx-0 xl:mt-0",
|
||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientSuspense>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function RecurringEventController({
|
||||
{recurringEventState && (
|
||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -89,7 +89,7 @@ export default function RecurringEventController({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -105,7 +105,7 @@ export default function RecurringEventController({
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<p className="mr-2 text-neutral-900">
|
||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
|
||||
{t("events", {
|
||||
count: recurringEventState.count,
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Icon, SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
@@ -10,7 +11,7 @@ function SkeletonLoader() {
|
||||
<SkeletonText className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
@@ -31,11 +32,11 @@ function SkeletonItem() {
|
||||
<div className="">
|
||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -16,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
|
||||
<>
|
||||
<div className="mt-6 flex flex-row">
|
||||
<div className="w-full">
|
||||
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-neutral-700">
|
||||
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
|
||||
{t("create_events_on")}
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormEvent, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
|
||||
type FormData = {
|
||||
bio: string;
|
||||
};
|
||||
@@ -21,13 +25,10 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
const { user } = props;
|
||||
const { t } = useLocale();
|
||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||
const bioRef = useRef<HTMLTextAreaElement>(null);
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({ defaultValues: { bio: user?.bio || "" } });
|
||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||
defaultValues: { bio: user?.bio || "" },
|
||||
});
|
||||
|
||||
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
|
||||
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
||||
const utils = trpc.useContext();
|
||||
@@ -113,7 +114,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
name="avatar"
|
||||
id="avatar"
|
||||
placeholder="URL"
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800"
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
|
||||
defaultValue={imageSrc}
|
||||
/>
|
||||
<div className="flex items-center px-4">
|
||||
@@ -138,25 +139,12 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="mt-8">
|
||||
<label htmlFor="bio" className="mb-2 block text-sm font-medium text-gray-700">
|
||||
{t("about")}
|
||||
</label>
|
||||
<TextArea
|
||||
{...register("bio", { required: true })}
|
||||
ref={bioRef}
|
||||
name="bio"
|
||||
id="bio"
|
||||
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||
defaultValue={user?.bio || undefined}
|
||||
onChange={(event) => {
|
||||
setValue("bio", event.target.value);
|
||||
}}
|
||||
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||
setText={(value: string) => setValue("bio", turndown(value))}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
/>
|
||||
{errors.bio && (
|
||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||
{t("required")}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
||||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
|
||||
@@ -19,7 +19,8 @@ interface IUserSettingsProps {
|
||||
const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { user, nextStep } = props;
|
||||
const { t } = useLocale();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess());
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { StepCard, Steps } from "../../..";
|
||||
import { StepCard, Steps } from "@calcom/ui";
|
||||
|
||||
export default function WizardLayout({
|
||||
children,
|
||||
@@ -0,0 +1,4 @@
|
||||
We don't have many layouts currently however, if they are relevant to a features - please put them in their relvant component folder
|
||||
in `web/components` if you believe they will be reused in other apps please place them in `packages/features/[feature]`
|
||||
|
||||
Thanks Sean 🔥
|
||||
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
|
||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
|
||||
@@ -177,7 +177,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
{t("continue")}
|
||||
@@ -186,13 +186,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
|
||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ShieldCheckIcon } from "@calcom/ui";
|
||||
import { ShieldCheckIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Icon, showToast, Switch } from "@calcom/ui";
|
||||
import { Badge, showToast, Switch } from "@calcom/ui";
|
||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
||||
|
||||
export function CalendarSwitch(props: {
|
||||
type: string;
|
||||
@@ -62,7 +63,7 @@ export function CalendarSwitch(props: {
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="flex space-x-2 py-1">
|
||||
<div className="flex space-x-2 py-1 rtl:space-x-reverse">
|
||||
<Switch
|
||||
key={props.externalId}
|
||||
name="enabled"
|
||||
@@ -74,7 +75,7 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<Icon.FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
|
||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
|
||||
@@ -170,7 +170,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
{t("continue")}
|
||||
@@ -179,13 +179,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ltr:ml-2 rtl:mr-2"
|
||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
|
||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import Link from "next/link";
|
||||
import { TeamPageProps } from "pages/team/[slug]";
|
||||
|
||||
@@ -6,6 +7,8 @@ import { Avatar } from "@calcom/ui";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
@@ -13,6 +16,8 @@ type MemberType = MembersType[number];
|
||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
return (
|
||||
<Link key={member.id} href={`/${member.username}`}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
||||
@@ -22,10 +27,19 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||
/>
|
||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
|
||||
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-neutral-500 dark:text-white">
|
||||
{member.bio || t("user_from_team", { user: member.name, team: teamName })}
|
||||
</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
|
||||
<div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
|
||||
{!isBioEmpty ? (
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
t("user_from_team", { user: member.name, team: teamName })
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -46,7 +60,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: TeamPageProps) => {
|
||||
const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -23,8 +22,9 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
|
||||
)}
|
||||
|
||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
||||
{props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>}
|
||||
</div>
|
||||
{props.loading && (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
||||
@@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
<div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
|
||||
import { Maybe } from "@calcom/trpc/server";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
||||
export type AvatarProps = {
|
||||
className?: string;
|
||||
size?: number;
|
||||
imageSrc?: Maybe<string>;
|
||||
title?: string;
|
||||
alt: string;
|
||||
gravatarFallbackMd5?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use AvatarSSR instead. Once, there is no usage of Avatar, AvatarSSR can be renamed.
|
||||
*/
|
||||
export default function Avatar(props: AvatarProps) {
|
||||
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
|
||||
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
|
||||
const avatar = (
|
||||
<AvatarPrimitive.Root>
|
||||
<AvatarPrimitive.Image
|
||||
src={imageSrc ?? undefined}
|
||||
alt={alt}
|
||||
className={classNames("rounded-full", `h-auto w-${size}`, props.className)}
|
||||
/>
|
||||
<AvatarPrimitive.Fallback delayMs={600}>
|
||||
{gravatarFallbackMd5 && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={defaultAvatarSrc({ md5: gravatarFallbackMd5 })} alt={alt} className={className} />
|
||||
)}
|
||||
</AvatarPrimitive.Fallback>
|
||||
</AvatarPrimitive.Root>
|
||||
);
|
||||
|
||||
return title ? (
|
||||
<Tooltip.Tooltip delayDuration={300}>
|
||||
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
|
||||
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
|
||||
<Tooltip.Arrow />
|
||||
{title}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Tooltip>
|
||||
) : (
|
||||
<>{avatar}</>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
||||
|
||||
export type AvatarGroupProps = {
|
||||
border?: string; // this needs to be the color of the parent container background, i.e.: border-white dark:border-gray-900
|
||||
size: number;
|
||||
truncateAfter?: number;
|
||||
items: {
|
||||
image: string;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
href?: string;
|
||||
}[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
||||
return (
|
||||
<ul className={classNames(props.className)}>
|
||||
{props.items.slice(0, props.truncateAfter).map((item, idx) => {
|
||||
if (item.image != null) {
|
||||
const avatar = (
|
||||
<AvatarSSR
|
||||
className={props.border}
|
||||
imageSrc={item.image}
|
||||
title={item.title}
|
||||
alt={item.alt || ""}
|
||||
size={props.size}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={idx} className="-mr-2 inline-block">
|
||||
{item.href ? (
|
||||
<Link href={item.href}>
|
||||
<a>{avatar}</a>
|
||||
</Link>
|
||||
) : (
|
||||
avatar
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarGroup;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { User } from "@prisma/client";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
export type AvatarProps = (
|
||||
| {
|
||||
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
|
||||
}
|
||||
| {
|
||||
user?: null;
|
||||
imageSrc: string;
|
||||
}
|
||||
) & {
|
||||
className?: string;
|
||||
size?: number;
|
||||
title?: string;
|
||||
href?: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
// defaultAvatarSrc from profile.tsx can't be used as it imports crypto
|
||||
function defaultAvatarSrc(md5: string) {
|
||||
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
|
||||
}
|
||||
|
||||
// An SSR Supported version of Avatar component.
|
||||
export function AvatarSSR(props: AvatarProps) {
|
||||
const { size, title } = props;
|
||||
|
||||
let imgSrc = "";
|
||||
let alt: string = props.alt;
|
||||
|
||||
if (props.user) {
|
||||
const user = props.user;
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
alt = alt || nameOrUsername;
|
||||
|
||||
if (user.avatar) {
|
||||
imgSrc = user.avatar;
|
||||
} else if (user.emailMd5) {
|
||||
imgSrc = defaultAvatarSrc(user.emailMd5);
|
||||
}
|
||||
} else {
|
||||
imgSrc = props.imageSrc;
|
||||
}
|
||||
|
||||
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
|
||||
|
||||
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
|
||||
return title ? (
|
||||
<Tooltip.Tooltip delayDuration={300}>
|
||||
<Tooltip.TooltipTrigger asChild>{avatar}</Tooltip.TooltipTrigger>
|
||||
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
|
||||
<Tooltip.Arrow />
|
||||
{title}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Tooltip>
|
||||
) : (
|
||||
<>{avatar}</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from "classnames";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
const EditableHeading = function EditableHeading({
|
||||
value,
|
||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
||||
<label className="min-w-8 relative inline-block">
|
||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||
{!isEditing && isReady && (
|
||||
<Icon.FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
|
||||
<FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
|
||||
)}
|
||||
<input
|
||||
{...passThroughProps}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Icon, Tooltip } from "@calcom/ui";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
import { FiInfo } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function InfoBadge({ content }: { content: string }) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip side="top" content={content}>
|
||||
<span title={content}>
|
||||
<Icon.FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
|
||||
<FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
|
||||
type="button"
|
||||
{...props}
|
||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||
<props.Icon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
<props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
||||
{props.children}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface ILinkTextProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
classNameChildren?: string;
|
||||
target?: string;
|
||||
}
|
||||
/**
|
||||
* This component had to be made in order to make i18n work with next/link
|
||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
||||
**/
|
||||
export const LinkText = (props: ILinkTextProps) => {
|
||||
const { target, href, children, classNameChildren, ...moreProps } = props;
|
||||
return (
|
||||
<Link href={href || ""} {...moreProps}>
|
||||
<a target={target || ""} className={classNameChildren}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
|
||||
const isEmbed = useIsEmbed();
|
||||
return (
|
||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
||||
<Link href={POWERED_BY_URL}>
|
||||
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||
{t("powered_by")}{" "}
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||
src="/cal-logo-word.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
}
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||
src="/cal-logo-word-dark.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
}
|
||||
</a>
|
||||
<Link
|
||||
href={POWERED_BY_URL}
|
||||
target="_blank"
|
||||
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||
{t("powered_by")}{" "}
|
||||
<img
|
||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||
src="/cal-logo-word.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
<img
|
||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||
src="/cal-logo-word-dark.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
|
||||
<div className="space-y-3">
|
||||
<div className="block sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-neutral-700">
|
||||
<Icon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
|
||||
<Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -11,17 +11,8 @@ import { User } from "@calcom/prisma/client";
|
||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
StarIconSolid,
|
||||
} from "@calcom/ui";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
||||
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
||||
|
||||
export enum UsernameChangeStatusEnum {
|
||||
UPGRADE = "UPGRADE",
|
||||
@@ -197,10 +188,10 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
<div className="flex justify-items-center">
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
</div>
|
||||
<div className="mt-2 flex rounded-md">
|
||||
<div className="flex rounded-md">
|
||||
<span
|
||||
className={classNames(
|
||||
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
|
||||
isInputUsernamePremium ? "border border-orange-400 " : "",
|
||||
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
|
||||
)}>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
||||
@@ -217,8 +208,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
className={classNames(
|
||||
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
|
||||
isInputUsernamePremium
|
||||
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400"
|
||||
: "border-1 focus:border-2",
|
||||
? "border border-orange-400 focus:border focus:border-orange-400"
|
||||
: "border focus:border",
|
||||
markAsError
|
||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
||||
: "border-l-gray-300",
|
||||
@@ -241,7 +232,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
usernameIsAvailable ? "" : ""
|
||||
)}>
|
||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,7 +250,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row">
|
||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiEdit2 className="m-auto h-6 w-6" />
|
||||
<FiEdit2 className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="mb-4 w-full px-4 pt-1">
|
||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||
@@ -293,7 +284,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
data-testid="go-to-billing"
|
||||
href={paymentLink}>
|
||||
<>
|
||||
{t("go_to_stripe_billing")} <Icon.FiExternalLink className="ml-1 h-4 w-4" />
|
||||
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
interface ICustomUsernameProps {
|
||||
currentUsername: string | undefined;
|
||||
@@ -77,10 +78,9 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
const ActionButtons = () => {
|
||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||
<div className="flex flex-row">
|
||||
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
type="button"
|
||||
className="mx-2"
|
||||
onClick={() => setOpenDialogSaveUsername(true)}
|
||||
data-testid="update-username-btn">
|
||||
{t("update")}
|
||||
@@ -88,7 +88,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
className="mx-2"
|
||||
onClick={() => {
|
||||
if (currentUsername) {
|
||||
setInputUsernameValue(currentUsername);
|
||||
@@ -110,26 +109,20 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
</div>
|
||||
<div className="mt-2 flex rounded-md">
|
||||
<span
|
||||
className={classNames(
|
||||
"inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"
|
||||
)}>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
||||
</span>
|
||||
<div className="flex rounded-md">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
<TextField
|
||||
ref={usernameRef}
|
||||
name="username"
|
||||
value={inputUsernameValue}
|
||||
addOnLeading={
|
||||
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
||||
}
|
||||
autoComplete="none"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="none"
|
||||
className={classNames(
|
||||
"mb-0 mt-0 h-6 rounded-md rounded-l-none",
|
||||
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
|
||||
markAsError
|
||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||
: ""
|
||||
@@ -141,18 +134,18 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
data-testid="username-input"
|
||||
/>
|
||||
{currentUsername !== inputUsernameValue && (
|
||||
<div className="absolute right-[2px] top-0 flex flex-row">
|
||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||
<span className={classNames("mx-2 py-2")}>
|
||||
{usernameIsAvailable ? <Icon.FiCheck className="mt-[2px] w-6" /> : <></>}
|
||||
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden md:inline">
|
||||
<div className="mt-5 hidden md:inline">
|
||||
<ActionButtons />
|
||||
</div>
|
||||
</div>
|
||||
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
||||
{markAsError && <p className="mt-1 text-xs text-red-500">{t("username_already_taken")}</p>}
|
||||
|
||||
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
|
||||
<div className="mt-2 flex justify-end md:hidden">
|
||||
@@ -163,7 +156,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
<DialogContent>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiEdit2 className="m-auto h-6 w-6" />
|
||||
<FiEdit2 className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="mb-4 w-full px-4 pt-1">
|
||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||
|
||||
@@ -21,7 +21,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "div" : "label",
|
||||
{
|
||||
className: "flex text-sm font-medium text-neutral-700",
|
||||
className: "flex text-sm font-medium text-gray-700",
|
||||
...(!descriptionAsLabel
|
||||
? {
|
||||
htmlFor: rest.id,
|
||||
@@ -39,7 +39,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||
{
|
||||
className: classNames(
|
||||
"relative flex items-start",
|
||||
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
|
||||
descriptionAsLabel ? "text-gray-700" : "text-gray-900"
|
||||
),
|
||||
},
|
||||
<>
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from "react";
|
||||
import { Props } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type CheckedSelectOption = {
|
||||
@@ -41,16 +41,17 @@ export const CheckedSelect = ({
|
||||
{...props}
|
||||
/>
|
||||
{value.map((option) => (
|
||||
<div key={option.value} className="border-1 border p-2 font-medium">
|
||||
<div key={option.value} className="border p-2 font-medium">
|
||||
<Avatar
|
||||
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2"
|
||||
className="inline ltr:mr-2 rtl:ml-2"
|
||||
size="sm"
|
||||
imageSrc={option.avatar}
|
||||
alt={option.label}
|
||||
/>
|
||||
{option.label}
|
||||
<Icon.FiX
|
||||
<FiX
|
||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-neutral-500"
|
||||
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
|
||||
import "react-date-picker/dist/DatePicker.css";
|
||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiCalendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
calendarIcon={<Icon.FiCalendar className="h-5 w-5 text-gray-500" />}
|
||||
calendarIcon={<FiCalendar className="h-5 w-5 text-gray-500" />}
|
||||
value={date}
|
||||
minDate={minDate}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { components, GroupBase, Props, SingleValue } from "react-select";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
export type LocationOption = {
|
||||
label: string;
|
||||
value: EventLocationType["type"];
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||
|
||||
export type GroupOptionType = GroupBase<LocationOption>;
|
||||
|
||||
const OptionWithIcon = ({
|
||||
icon,
|
||||
isSelected,
|
||||
label,
|
||||
}: {
|
||||
icon?: string;
|
||||
isSelected?: boolean;
|
||||
label: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
|
||||
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
|
||||
return (
|
||||
<Select<LocationOption>
|
||||
name="location"
|
||||
components={{
|
||||
Option: (props) => (
|
||||
<components.Option {...props}>
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
|
||||
</components.Option>
|
||||
),
|
||||
SingleValue: (props) => (
|
||||
<components.SingleValue {...props}>
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||
</components.SingleValue>
|
||||
),
|
||||
}}
|
||||
formatOptionLabel={(e) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
|
||||
<span>{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
|
||||
<div className="block sm:flex">
|
||||
{!!label && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-neutral-700">
|
||||
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ function Select<
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("text-sm", className)}
|
||||
className={classNames("border-0 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
||||
onChange?: (value: string) => void;
|
||||
defaultChecked?: boolean;
|
||||
};
|
||||
|
||||
const RadioArea = (props: RadioAreaProps) => {
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
||||
props.checked && "border-brand",
|
||||
props.className
|
||||
)}>
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (typeof props.onChange === "function") {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
checked={props.checked}
|
||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
type="radio"
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
type ChildrenProps = {
|
||||
props: RadioAreaProps;
|
||||
children?: ReactNode;
|
||||
};
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
||||
children: ChildrenProps | ChildrenProps[];
|
||||
name?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
||||
|
||||
const changeHandler = (value: string, idx: number) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setCheckedIdx(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...passThroughProps}>
|
||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
||||
if (checkedIdx === null && child.props.defaultChecked) {
|
||||
setCheckedIdx(idx);
|
||||
}
|
||||
return (
|
||||
<Item
|
||||
{...child.props}
|
||||
key={idx}
|
||||
name={name}
|
||||
checked={idx === checkedIdx}
|
||||
onChange={(value: string) => changeHandler(value, idx)}>
|
||||
{child.props.children}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
|
||||
|
||||
interface OptionProps
|
||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
options,
|
||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
||||
placeholder = t("select"),
|
||||
} = props;
|
||||
|
||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
||||
options.find((option: OptionProps) => option.value === value)?.label;
|
||||
|
||||
return (
|
||||
<Collapsible className={classNames("w-full", props.className)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
|
||||
disabled && "cursor-default bg-gray-200 focus:ring-0 "
|
||||
)}>
|
||||
{getLabel(props.value) ?? placeholder}
|
||||
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
||||
{options.map((option) => (
|
||||
<RadioArea
|
||||
{...option}
|
||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
|
||||
defaultChecked={props.value === option.value}>
|
||||
<strong className="mb-1 block">{option.label}</strong>
|
||||
<p>{option.description}</p>
|
||||
</RadioArea>
|
||||
))}
|
||||
</RadioAreaGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user