Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3a67e5617e | ||
|
|
f401aa7669 | ||
|
|
a6e0678e59 | ||
|
|
e832015f26 | ||
|
|
5e86881e15 | ||
|
|
3b71f98460 | ||
|
|
144d57adb2 | ||
|
|
e1f1c15318 | ||
|
|
d88b1b2f8f | ||
|
|
2e61c15f2c | ||
|
|
df73473c7f | ||
|
|
7167973afd | ||
|
|
3c17bc2d1a | ||
|
|
a9a295dc54 | ||
|
+2 |
21dd1f4e95 | ||
|
|
5d775bd4a3 | ||
|
|
4e02b22472 | ||
|
|
4686d3ff48 | ||
|
|
02e9d8d45f | ||
|
|
7ce5096f13 | ||
|
|
83e51e9b9d | ||
|
|
dca3278c40 | ||
|
|
7501635fbf | ||
|
|
80c96ea5ac | ||
|
|
8c001e0d34 | ||
|
|
bf275497a2 | ||
|
|
6e351a4b20 | ||
|
|
5360cea3bf | ||
|
+2 |
2f36985462 | ||
|
|
6b26e9a7de | ||
|
|
d7655f0521 | ||
|
|
f4ed345a87 | ||
|
|
c3122c82e4 | ||
|
|
cc9b49131e | ||
|
|
190db1a0f6 | ||
|
|
bd48a72e99 | ||
|
|
a6836a1488 | ||
|
|
2b67ea9635 | ||
|
|
a9117c2d36 | ||
|
+1 |
10d3b5f1b9 | ||
|
|
b757cb6ba1 | ||
|
|
5968e0aa19 | ||
|
|
a06fffc4ec | ||
|
|
3312c84e7c | ||
|
|
d86710d5e9 | ||
|
|
2d8e65c160 | ||
|
|
f74eaf7cb3 | ||
|
|
bcfd57c953 | ||
|
|
c38588b93d | ||
|
|
32ed43574d | ||
|
|
38a5a22b9e | ||
|
|
2120e301c3 | ||
|
|
dd0aec4e97 | ||
|
|
3815b86258 | ||
|
|
7a6545bd03 | ||
|
|
11f467af5d | ||
|
|
0930cf50b6 | ||
|
|
c37cddd0b7 | ||
|
|
09253f83bf | ||
|
|
1cdcebac01 | ||
|
|
58270fa653 | ||
|
|
1a26a254ab | ||
|
|
a36d15894d | ||
|
|
cf077fd33c | ||
|
|
0c979d3604 | ||
|
|
33bc786bb3 | ||
|
|
a7f16462db | ||
|
|
2c7fcbcf51 | ||
|
|
d3e4d26fa5 | ||
|
|
3be6c05722 | ||
|
|
e293e590bd | ||
|
+1 |
d8c1c103a1 | ||
|
|
19d00e691c | ||
|
|
78aa4aecef | ||
|
|
3ab002e547 | ||
|
|
7f461bc275 | ||
|
|
571176102e | ||
|
|
8f1d3fbc50 | ||
|
|
5e88ad50ea | ||
|
|
056406bdd9 | ||
|
|
58af5a18a4 | ||
|
|
90b6581d76 | ||
|
|
b33dcc8841 | ||
|
|
d951a5b872 | ||
|
|
7122a9cfbf | ||
|
|
f1b92c49fc | ||
|
|
ef3ad36c2c | ||
|
|
072ce452b7 | ||
|
|
0e0940f5f0 | ||
|
|
310936125d | ||
|
|
a9df3309b9 | ||
|
|
148d658f14 | ||
|
|
242d571d7d | ||
|
|
dff49ec28a | ||
|
|
0c746ec10f | ||
|
|
901f54ca3a | ||
|
|
16fda5f599 | ||
|
|
d3e9adf382 | ||
|
|
cf559e0726 | ||
|
|
8447b1bbd5 | ||
|
|
0d03006653 | ||
|
|
d44c5fd8b0 | ||
|
|
74aa366f1e | ||
|
|
7ebea57602 | ||
|
|
26d675e162 | ||
|
|
e16d29e916 | ||
|
|
0e89fb9f6a | ||
|
|
43fb410a28 | ||
|
|
b8bbfe8a3f | ||
|
|
1f74fc394a | ||
|
|
9a12b5a0ac | ||
|
|
2ce2c1074b | ||
|
|
0ef80da260 | ||
|
|
1e50488cfe | ||
|
|
ae9f66c9ef | ||
|
|
798a4b0090 | ||
|
|
dcd9ff72b7 | ||
|
|
0b5a0186bc | ||
|
|
d9846dca4c | ||
|
|
2fd606c38e | ||
|
|
c066a0cba9 | ||
|
|
568cc22c60 | ||
|
|
b3243c53dc | ||
|
|
80ffcf8b58 | ||
|
+1 |
77bf14a797 | ||
|
|
b188368e8f | ||
|
|
e35631aee3 | ||
|
|
733a06a672 | ||
|
|
2de3e7fc59 | ||
|
|
3137a188a0 | ||
|
|
a2a4ff934f | ||
|
|
eda1d5f393 | ||
|
|
93a14b190e | ||
|
|
2a6dc340df | ||
|
|
9cff1e3439 | ||
|
|
bb9cbd6582 | ||
|
|
81721e7bb9 | ||
|
|
454311dd52 | ||
|
|
7df04c1d50 | ||
|
|
d9a8ab3864 | ||
|
|
e3a7fd006b | ||
|
|
38d8852819 | ||
|
|
c0f34a39d1 | ||
|
|
e09dd04e7f | ||
|
|
1c2c6a62e6 | ||
|
|
33271a88b1 | ||
|
|
4086a666ae | ||
|
|
b31b8cc6df | ||
|
|
6dce57440e | ||
|
|
22a030afab | ||
|
|
e04d81cf11 | ||
|
|
bd6888b758 | ||
|
|
ccc2bdd25e | ||
|
|
a36b46bd81 | ||
|
|
a94d51c4bd | ||
|
|
3854b9c58e | ||
|
|
ba6963573e | ||
|
|
596f723cff | ||
|
|
c408d71a53 | ||
|
|
d64400d66b | ||
|
|
16eede9bb0 | ||
|
|
ba5d448811 | ||
|
|
1070cc2892 | ||
|
|
df91425643 | ||
|
|
c98bafcd52 | ||
|
|
9441a79def | ||
|
|
994df45a19 | ||
|
|
c4662d79e1 | ||
|
|
50cd1fbd83 | ||
|
|
196549c124 | ||
|
|
91274d8ee0 | ||
|
|
fa026ee393 | ||
|
|
5793901b03 | ||
|
|
030f1c6990 | ||
|
|
b201067f86 | ||
|
|
cb96b12108 |
+10
-1
@@ -11,6 +11,10 @@
|
|||||||
# - ZOOM
|
# - ZOOM
|
||||||
# - GIPHY
|
# - GIPHY
|
||||||
# - VITAL
|
# - VITAL
|
||||||
|
# - ZAPIER
|
||||||
|
# - LARK
|
||||||
|
# - WEB3
|
||||||
|
# - SALESFORCE
|
||||||
|
|
||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
@@ -88,7 +92,7 @@ VITAL_REGION="us"
|
|||||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
||||||
ZAPIER_INVITE_LINK=""
|
ZAPIER_INVITE_LINK=""
|
||||||
|
|
||||||
# - LARK
|
# - LARK
|
||||||
# Needed to enable Lark Calendar integration and Login with Lark
|
# Needed to enable Lark Calendar integration and Login with Lark
|
||||||
# @see <https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g>
|
# @see <https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g>
|
||||||
LARK_OPEN_APP_ID=""
|
LARK_OPEN_APP_ID=""
|
||||||
@@ -100,4 +104,9 @@ LARK_OPEN_VERIFICATION_TOKEN=""
|
|||||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
|
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
|
||||||
ALCHEMY_API_KEY=""
|
ALCHEMY_API_KEY=""
|
||||||
INFURA_API_KEY=""
|
INFURA_API_KEY=""
|
||||||
|
|
||||||
|
# - SALESFORCE
|
||||||
|
# Used for the Salesforce (Sales Cloud) app
|
||||||
|
SALESFORCE_CONSUMER_KEY=""
|
||||||
|
SALESFORCE_CONSUMER_SECRET=""
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
|
|||||||
+10
-3
@@ -7,14 +7,13 @@
|
|||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE *************************************************************************************************
|
||||||
# Set this value to 'agree' to accept our license:
|
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||||
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
|
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - 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.
|
# To enable enterprise-only features, fill your license key in here.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
@@ -86,6 +85,9 @@ SENDGRID_EMAIL=
|
|||||||
TWILIO_SID=
|
TWILIO_SID=
|
||||||
TWILIO_TOKEN=
|
TWILIO_TOKEN=
|
||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
|
TWILIO_PHONE_NUMBER=
|
||||||
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
|
TWILIO_VERIFY_SID=
|
||||||
|
|
||||||
# This is used so we can bypass emails in auth flows for E2E testing
|
# 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
|
# Set it to "1" if you need to run E2E tests locally
|
||||||
@@ -140,3 +142,8 @@ SENDGRID_SYNC_API_KEY=
|
|||||||
# Sentry
|
# Sentry
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
||||||
|
|
||||||
|
# Change your Brand
|
||||||
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
|
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||||
|
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||||
|
|||||||
@@ -3,3 +3,10 @@
|
|||||||
|
|
||||||
automerge:
|
automerge:
|
||||||
- apps/web/public/static/locales/**/*.json
|
- apps/web/public/static/locales/**/*.json
|
||||||
|
|
||||||
|
"❗️ migrations":
|
||||||
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
"❗️ .env changes":
|
||||||
|
- .env.example
|
||||||
|
- .env.appStore.example
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Add bugs to Bug Hunting
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-to-project:
|
|
||||||
name: Add issue to project
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/add-to-project@v0.1.0
|
|
||||||
with:
|
|
||||||
project-url: https://github.com/orgs/calcom/projects/9
|
|
||||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
labeled: 🙋🏻♂️help wanted
|
|
||||||
@@ -12,8 +12,5 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- 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:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
ref: main # Always checkout main even for tagged releases
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- run: git push origin +main:production
|
- run: git push origin +main:production
|
||||||
|
|||||||
Vendored
+2
-1
@@ -6,5 +6,6 @@
|
|||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"spellright.language": ["en"],
|
"spellright.language": ["en"],
|
||||||
"spellright.documentTypes": ["markdown", "typescript"]
|
"spellright.documentTypes": ["markdown", "typescript"],
|
||||||
|
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ You can deploy Cal on [Railway](https://railway.app/) using the button above. Th
|
|||||||
|
|
||||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ following
|
|||||||
4. Fill in any information you want in the "App info" tab
|
4. Fill in any information you want in the "App info" tab
|
||||||
5. Go to tab "Auth"
|
5. Go to tab "Auth"
|
||||||
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
||||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspotothercalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
||||||
9. Click the "Save" button at the bottom footer.
|
9. Click the "Save" button at the bottom footer.
|
||||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||||
@@ -419,14 +419,18 @@ following
|
|||||||
2. Click ‘Get a Twilio phone number’
|
2. Click ‘Get a Twilio phone number’
|
||||||
3. Copy Account SID to your .env file into the TWILIO_SID field
|
3. Copy Account SID to your .env file into the TWILIO_SID field
|
||||||
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
||||||
5. Create a messaging service (Develop -> Messaging -> Services)
|
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
|
||||||
6. Choose any name for the messaging service
|
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
|
||||||
7. Click 'Add Senders'
|
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||||
8. Choose phone number as sender type
|
8. Choose any name for the messaging service
|
||||||
9. Add the listed phone number
|
9. Click 'Add Senders'
|
||||||
10. Leave all other fields as they are
|
10. Choose phone number as sender type
|
||||||
11. Complete setup and click ‘View my new Messaging Service’
|
11. Add the listed phone number
|
||||||
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
12. Leave all other fields as they are
|
||||||
|
13. Complete setup and click ‘View my new Messaging Service’
|
||||||
|
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||||
|
15. Create a verify service
|
||||||
|
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* This is a Playwright script Checkly generated for you based on your Vercel project.
|
||||||
|
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Create a Chromium browser
|
||||||
|
const { chromium } = require("playwright");
|
||||||
|
|
||||||
|
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
|
||||||
|
async function run() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const targetUrl = process.env.ENVIRONMENT_URL || "https://cal.com";
|
||||||
|
await page.goto(`${targetUrl}/rick/test-location-link`);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="day"][data-disabled="false"]');
|
||||||
|
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="time"]');
|
||||||
|
await page.click('[data-testid="time"]');
|
||||||
|
|
||||||
|
await page.waitForSelector("#name");
|
||||||
|
await page.click("#name");
|
||||||
|
|
||||||
|
await page.type("#name", "Calcom");
|
||||||
|
|
||||||
|
await page.type('[name="email"]', "cal@cal.com");
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="confirm-book-button"]');
|
||||||
|
await page.click('[data-testid="confirm-book-button"]');
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -33,11 +33,6 @@
|
|||||||
"generator": "secret",
|
"generator": "secret",
|
||||||
"required": "true"
|
"required": "true"
|
||||||
},
|
},
|
||||||
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
|
||||||
"description": "License consent",
|
|
||||||
"value": "true",
|
|
||||||
"required": "true"
|
|
||||||
},
|
|
||||||
"CRON_API_KEY": {
|
"CRON_API_KEY": {
|
||||||
"description": "ApiKey for cronjobs",
|
"description": "ApiKey for cronjobs",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
|||||||
+1
-1
Submodule apps/api updated: 0f5017010f...604d937661
+1
-1
Submodule apps/console updated: 2219900e06...66e2663070
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
stories: [
|
stories: [
|
||||||
"../intro.stories.mdx",
|
"../intro.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
"../../../packages/ui/components/**/*.stories.mdx",
|
||||||
|
"../../../packages/features/**/*.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
@@ -50,6 +51,21 @@ module.exports = {
|
|||||||
vm: false,
|
vm: false,
|
||||||
zlib: 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;
|
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 = () => {
|
window.getEmbedNamespace = () => {
|
||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function VariantsTable({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isDark &&
|
isDark &&
|
||||||
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#22252A]"
|
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
|
||||||
)}>
|
)}>
|
||||||
<div className="z-1 relative mx-auto w-full max-w-[1200px] overflow-auto pr-8 pt-6">
|
<div className="z-1 relative mx-auto w-full max-w-[1200px] overflow-auto pr-8 pt-6">
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
.sbdocs {
|
||||||
font-family: 'Inter var' !important;
|
font-family: 'Inter var' !important;
|
||||||
padding: 0!important;
|
padding: 0!important;
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
|
|
||||||
/** Docs table **/
|
/** Docs table **/
|
||||||
.custom-args-wrapper{
|
.custom-args-wrapper{
|
||||||
max-height: 600px;
|
max-height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -187,3 +189,7 @@
|
|||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-story {
|
||||||
|
padding: 24px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
declare module "@wojtekmaj/react-daterange-picker/dist/entry.nostyle" {
|
|
||||||
import { CalendarProps } from "react-calendar";
|
|
||||||
export type DateRangePickerCalendarProps = Omit<
|
|
||||||
CalendarProps,
|
|
||||||
"calendarClassName" | "onChange" | "value"
|
|
||||||
> & {
|
|
||||||
calendarClassName?: string;
|
|
||||||
onChange: (value: [Date, Date]) => void;
|
|
||||||
value: [Date, Date];
|
|
||||||
clearIcon: JSX.Element | null;
|
|
||||||
calendarIcon: JSX.Element | null;
|
|
||||||
rangeDivider: JSX.Element | null;
|
|
||||||
};
|
|
||||||
export default function DateRangePicker(props: DateRangePickerCalendarProps): JSX.Element;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|||||||
@@ -4,14 +4,22 @@ import { NextRouter, useRouter } from "next/router";
|
|||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
||||||
import { components, ControlProps } from "react-select";
|
import { components, ControlProps } from "react-select";
|
||||||
|
|
||||||
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import {
|
||||||
import { Button } from "@calcom/ui/components";
|
Button,
|
||||||
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
Dialog,
|
||||||
import { HorizontalTabs, showToast, Switch } from "@calcom/ui/v2";
|
DialogClose,
|
||||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
DialogContent,
|
||||||
|
HorizontalTabs,
|
||||||
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
Icon,
|
||||||
|
InputLeading,
|
||||||
|
Label,
|
||||||
|
showToast,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
TextField,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
import ColorPicker from "@components/ui/colorpicker";
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
@@ -36,6 +44,7 @@ type PreviewState = {
|
|||||||
palette: {
|
palette: {
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
@@ -117,10 +126,12 @@ const getEmbedUIInstructionString = ({
|
|||||||
apiName,
|
apiName,
|
||||||
theme,
|
theme,
|
||||||
brandColor,
|
brandColor,
|
||||||
|
hideEventTypeDetails,
|
||||||
}: {
|
}: {
|
||||||
apiName: string;
|
apiName: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
theme = theme !== "auto" ? theme : undefined;
|
theme = theme !== "auto" ? theme : undefined;
|
||||||
return getInstructionString({
|
return getInstructionString({
|
||||||
@@ -133,6 +144,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
brandColor,
|
brandColor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -245,18 +257,21 @@ const getEmbedTypeSpecificString = ({
|
|||||||
apiName: string;
|
apiName: string;
|
||||||
theme: PreviewState["theme"];
|
theme: PreviewState["theme"];
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "cal",
|
apiName: "cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "Cal",
|
apiName: "Cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!frameworkCodes[embedType]) {
|
if (!frameworkCodes[embedType]) {
|
||||||
@@ -489,7 +504,9 @@ const tabs = [
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
|
<small className="flex py-4 text-neutral-500">
|
||||||
|
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="embed-code"
|
data-testid="embed-code"
|
||||||
@@ -610,13 +627,13 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<DialogContent type="creation" useOwnActionButtons size="lg">
|
<DialogContent type="creation" size="lg">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site")}
|
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@@ -679,6 +696,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
theme: Theme.auto,
|
theme: Theme.auto,
|
||||||
floatingPopup: {},
|
floatingPopup: {},
|
||||||
elementClick: {},
|
elementClick: {},
|
||||||
|
hideEventTypeDetails: false,
|
||||||
palette: {
|
palette: {
|
||||||
brandColor: "#000000",
|
brandColor: "#000000",
|
||||||
},
|
},
|
||||||
@@ -743,6 +761,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
name: "ui",
|
name: "ui",
|
||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
...previewState.palette,
|
...previewState.palette,
|
||||||
@@ -788,7 +807,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons>
|
<DialogContent size="xl" className="p-0.5" type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
@@ -801,7 +820,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<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" : "")}>
|
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
@@ -828,7 +847,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="text-sm">Embed Window Sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center">
|
||||||
<TextField
|
<TextField
|
||||||
name="width"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
required
|
required
|
||||||
value={previewState.inline.width}
|
value={previewState.inline.width}
|
||||||
@@ -850,7 +868,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<span className="p-2">×</span>
|
<span className="p-2">×</span>
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
name="height"
|
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -878,7 +895,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className="mb-2 text-sm">Button Text</div>
|
<div className="mb-2 text-sm">Button Text</div>
|
||||||
{/* Default Values should come from preview iframe */}
|
{/* Default Values should come from preview iframe */}
|
||||||
<TextField
|
<TextField
|
||||||
name="buttonText"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -994,6 +1010,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
|
<div className="mb-4 flex items-center justify-start space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={previewState.hideEventTypeDetails}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
hideEventTypeDetails: checked,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||||
|
</div>
|
||||||
<Label className="">
|
<Label className="">
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -1086,9 +1116,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{t("copy_code")}
|
{t("copy_code")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<DialogClose asChild>
|
<DialogClose />
|
||||||
<Button color="secondary">{t("Close")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
|
||||||
|
|
||||||
import { Area, getCroppedImg } from "@lib/cropImage";
|
import { Area, getCroppedImg } from "@lib/cropImage";
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
import { useFileReader } from "@lib/hooks/useFileReader";
|
||||||
@@ -157,12 +156,8 @@ export default function ImageUploader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<DialogClose asChild>
|
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
|
||||||
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
|
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* @deprecated Use custom Skeletons instead
|
* @deprecated Use custom Skeletons instead
|
||||||
**/
|
**/
|
||||||
export { default } from "@calcom/ui/Loader";
|
export { Loader as default } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
|
import { ErrorBoundary, Icon, Shell } from "@calcom/ui";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import Shell from "@calcom/ui/Shell";
|
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import React from "react";
|
|
||||||
import { OptionProps } from "react-select";
|
import { OptionProps } from "react-select";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button, Icon, Select } from "@calcom/ui";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { Select } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -64,12 +61,10 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<Select
|
<Select
|
||||||
name="additionalCalendar"
|
name="additionalCalendar"
|
||||||
placeholder={
|
placeholder={
|
||||||
<div className="flex justify-start text-black">
|
<Button StartIcon={Icon.FiPlus} color="secondary">
|
||||||
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
|
{t("add")}
|
||||||
<p>{t("add")}</p>
|
</Button>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
className="min-w-44"
|
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -83,11 +78,16 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
width: "max-content",
|
width: "max-content",
|
||||||
right: "0",
|
right: "0",
|
||||||
}),
|
}),
|
||||||
|
indicatorSeparator: (defaultStyles) => ({
|
||||||
|
...defaultStyles,
|
||||||
|
display: "none",
|
||||||
|
}),
|
||||||
control: (defaultStyles) => ({
|
control: (defaultStyles) => ({
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
padding: "0",
|
padding: "0",
|
||||||
border: "0",
|
border: "0",
|
||||||
borderRadius: "inherit",
|
borderRadius: "6px",
|
||||||
|
minHeight: "auto",
|
||||||
}),
|
}),
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
dropdownIndicator: (defaultStyles) => ({
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
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 LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||||
import classNames from "@calcom/lib/classNames";
|
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import { showToast, SkeletonText } from "@calcom/ui/v2";
|
|
||||||
import { SkeletonButton, Shell } from "@calcom/ui/v2";
|
|
||||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
|
||||||
|
|
||||||
import HeadSeo from "@components/seo/head-seo";
|
import HeadSeo from "@components/seo/head-seo";
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ const Component = ({
|
|||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.setupPending) return;
|
if (data?.setupPending) return;
|
||||||
showToast(t("app_successfully_installed"), "success");
|
showToast(t("app_successfully_installed"), "success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -68,7 +66,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 (
|
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">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
@@ -123,6 +123,7 @@ const Component = ({
|
|||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
|
...props,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type, variant, slug });
|
mutation.mutate({ type, variant, slug });
|
||||||
},
|
},
|
||||||
@@ -161,6 +162,7 @@ const Component = ({
|
|||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
|
...props,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type, variant, slug });
|
mutation.mutate({ type, variant, slug });
|
||||||
},
|
},
|
||||||
@@ -195,7 +197,7 @@ const Component = ({
|
|||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||||
<span>
|
<span>
|
||||||
{price === 0 ? (
|
{price === 0 ? (
|
||||||
"Free"
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Intl.NumberFormat("en-US", {
|
{Intl.NumberFormat("en-US", {
|
||||||
@@ -273,8 +275,10 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
<span className="leading-1 block text-xs text-gray-500">{t("every_app_published")}</span>
|
<span className="leading-1 block text-xs text-gray-500">
|
||||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
{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")}
|
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ import Link from "next/link";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import {
|
||||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
Alert,
|
||||||
import { Button } from "@calcom/ui/components";
|
Button,
|
||||||
import { Alert, EmptyScreen } from "@calcom/ui/v2";
|
EmptyScreen,
|
||||||
import { List } from "@calcom/ui/v2/core/List";
|
Icon,
|
||||||
import { ShellSubHeading } from "@calcom/ui/v2/core/Shell";
|
List,
|
||||||
import Switch from "@calcom/ui/v2/core/Switch";
|
ShellSubHeading,
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
showToast,
|
||||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
SkeletonLoader,
|
||||||
|
Switch,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -156,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
if (!data.connectedCalendars.length) {
|
if (!data.connectedCalendars.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
@@ -165,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
slug={item.integration.slug}
|
slug={item.integration.slug}
|
||||||
title={item.integration.title}
|
title={item.integration.title}
|
||||||
logo={item.integration.logo}
|
logo={item.integration.logo}
|
||||||
description={item.primary?.externalId || "No external Id"}
|
description={item.primary?.email ?? item.integration.description}
|
||||||
separate={true}
|
separate={true}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-32 justify-end">
|
<div className="flex w-32 justify-end">
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
||||||
import { showToast } from "@calcom/ui/v2";
|
|
||||||
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ function IntegrationListItem(props: {
|
|||||||
expanded={!!props.children}
|
expanded={!!props.children}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
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" : ""
|
highlight ? "bg-yellow-100" : ""
|
||||||
)}>
|
)}>
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import useDigitInput from "react-digit-input";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Label, Input } from "@calcom/ui/form/fields";
|
import { Label, Input } from "@calcom/ui";
|
||||||
|
|
||||||
export default function TwoFactor({ center = true }) {
|
export default function TwoFactor({ center = true }) {
|
||||||
const [value, onChange] = useState("");
|
const [value, onChange] = useState("");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui/v2";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
import { Icon, Tooltip } from "@calcom/ui";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
@@ -15,14 +15,18 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||||
<img
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
src={eventLocationType.iconUrl}
|
<Icon.FiLink className="dark:text-darkgray-600 mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 " />
|
||||||
className={classNames(
|
) : (
|
||||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
<img
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
src={eventLocationType.iconUrl}
|
||||||
)}
|
className={classNames(
|
||||||
alt={`${eventLocationType.label} icon`}
|
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||||
/>
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||||
|
)}
|
||||||
|
alt={`${eventLocationType.label} icon`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tooltip content={locationKeyToString(location)}>
|
<Tooltip content={locationKeyToString(location)}>
|
||||||
<p className="truncate">{locationKeyToString(location)}</p>
|
<p className="truncate">{locationKeyToString(location)}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
@@ -58,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-bookinglight font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
@@ -102,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon, Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import { UserAvatars } from "@components/booking/UserAvatars";
|
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||||
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||||
@@ -39,7 +42,34 @@ interface Props {
|
|||||||
|
|
||||||
const BookingDescription: FC<Props> = (props) => {
|
const BookingDescription: FC<Props> = (props) => {
|
||||||
const { profile, eventType, isBookingPage = false, children } = props;
|
const { profile, eventType, isBookingPage = false, children } = props;
|
||||||
|
const { date: bookingDate } = useRouterQuery("date");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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;
|
||||||
|
if (rcThreshold) {
|
||||||
|
if (isBookingPage) {
|
||||||
|
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
|
||||||
|
requiresConfirmation = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requiresConfirmationText = t("requires_confirmation_threshold", {
|
||||||
|
...rcThreshold,
|
||||||
|
unit: rcThreshold.unit.slice(0, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserAvatars
|
<UserAvatars
|
||||||
@@ -55,7 +85,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
|
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="dark:text-darkgray-600 flex flex-col space-y-3 text-sm font-medium text-gray-600">
|
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
|
||||||
{eventType?.description && (
|
{eventType?.description && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -75,29 +105,61 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center",
|
"items-top flex",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
</div>
|
</div>
|
||||||
{t("requires_confirmation")}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AvailableEventLocations
|
<AvailableEventLocations
|
||||||
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
||||||
/>
|
/>
|
||||||
<p
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-sm font-medium",
|
"flex flex-nowrap text-sm font-medium",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
<Icon.FiClock
|
||||||
{eventType.length} {t("minutes")}
|
className={classNames(
|
||||||
</p>
|
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
|
||||||
|
isBookingPage && "mt-[2px]"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{eventType.metadata?.multipleDuration !== undefined ? (
|
||||||
|
!isBookingPage ? (
|
||||||
|
<ul className="-mt-1 flex flex-wrap gap-1">
|
||||||
|
{eventType.metadata.multipleDuration.map((dur, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<Badge
|
||||||
|
variant="gray"
|
||||||
|
size="lg"
|
||||||
|
className={classNames(
|
||||||
|
duration === dur.toString()
|
||||||
|
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||||
|
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
|
||||||
|
"cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setDuration(dur);
|
||||||
|
}}>
|
||||||
|
{dur} {t("minute_timeUnit")}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
`${duration} ${t("minutes")}`
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
`${eventType.length} ${t("minutes")}`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BookingStatus } from "@prisma/client";
|
import { BookingStatus } from "@prisma/client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState, useMemo } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
@@ -9,14 +9,20 @@ import { formatTime } from "@calcom/lib/date-fns";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import {
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
Badge,
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
Button,
|
||||||
import { Button } from "@calcom/ui/components/button";
|
Dialog,
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
DialogClose,
|
||||||
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
|
DialogContent,
|
||||||
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
DialogFooter,
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
DialogHeader,
|
||||||
|
Icon,
|
||||||
|
MeetingTimeInTimezones,
|
||||||
|
showToast,
|
||||||
|
TextArea,
|
||||||
|
Tooltip,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
||||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||||
cancel all remaining bookings or just that booking instance. */
|
cancel all remaining bookings or just that booking instance. */
|
||||||
href: `/success?uid=${booking.uid}&cancel=true${
|
href: `/booking/${booking.uid}?cancel=true${
|
||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}`,
|
}`,
|
||||||
icon: Icon.FiX,
|
icon: Icon.FiX,
|
||||||
@@ -152,6 +158,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPast && isPending && !isConfirmed) {
|
||||||
|
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||||
|
}
|
||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||||
@@ -189,11 +199,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const onClickTableData = () => {
|
const onClickTableData = () => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${booking.uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid: booking.uid,
|
|
||||||
allRemainingBookings: isTabRecurring,
|
allRemainingBookings: isTabRecurring,
|
||||||
listingStatus: booking.listingStatus,
|
|
||||||
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -231,9 +239,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose>
|
<DialogClose />
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
@@ -246,7 +252,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||||
<td
|
<td
|
||||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
onClick={onClickTableData}>
|
onClick={onClickTableData}>
|
||||||
@@ -273,9 +279,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{booking.eventType.team.name}
|
{booking.eventType.team.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{booking.paid && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{t("pending_payment")}
|
{t("paid")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
@@ -336,12 +342,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!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 rtl:mr-2 sm:inline-flex" variant="orange">
|
||||||
|
{t("pending_payment")}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<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}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -524,13 +532,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;
|
export default BookingListItem;
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||||
import { Button, TextArea } from "@calcom/ui/components";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -38,7 +37,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div>
|
<div className="mt-8">
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
<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" />
|
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
@@ -90,11 +89,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.status >= 200 && res.status < 300) {
|
if (res.status >= 200 && res.status < 300) {
|
||||||
await router.push(
|
await router.replace(router.asPath);
|
||||||
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
|
|
||||||
profile.slug
|
|
||||||
}&team=${team ? 1 : 0}&allRemainingBookings=${allRemainingBookings}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(
|
setError(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui/v2";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect";
|
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const UserAvatars = ({
|
|||||||
users,
|
users,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
profile: { image: string | null; name?: string | null };
|
profile: { image: string | null; name?: string | null; username?: string | null };
|
||||||
showMembers: boolean;
|
showMembers: boolean;
|
||||||
users: { username: string | null; name?: string | null }[];
|
users: { username: string | null; name?: string | null }[];
|
||||||
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
|
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
|
||||||
@@ -17,15 +17,26 @@ export const UserAvatars = ({
|
|||||||
border="border-2 dark:border-darkgray-100 border-white"
|
border="border-2 dark:border-darkgray-100 border-white"
|
||||||
items={
|
items={
|
||||||
[
|
[
|
||||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
{
|
||||||
|
image: profile.image,
|
||||||
|
alt: profile.name,
|
||||||
|
title: profile.name,
|
||||||
|
href: profile.username ? `${CAL_URL}/${profile.username}` : undefined,
|
||||||
|
},
|
||||||
...(showMembers
|
...(showMembers
|
||||||
? users.map((user) => ({
|
? users.map((user) => ({
|
||||||
title: user.name,
|
title: user.name,
|
||||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||||
alt: user.name || undefined,
|
alt: user.name || undefined,
|
||||||
|
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
|
||||||
}))
|
}))
|
||||||
: []),
|
: []),
|
||||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
].filter((item) => !!item.image) as {
|
||||||
|
image: string;
|
||||||
|
alt?: string;
|
||||||
|
title?: string;
|
||||||
|
href?: string;
|
||||||
|
}[]
|
||||||
}
|
}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
truncateAfter={props.truncateAfter}
|
truncateAfter={props.truncateAfter}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventType } from "@prisma/client";
|
import { EventType } from "@prisma/client";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { TFunction } from "next-i18next";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
@@ -14,12 +13,12 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
|
|||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
@@ -28,13 +27,10 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
|||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon, DatePicker } from "@calcom/ui";
|
||||||
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
// import { timeZone } from "@lib/clock";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
@@ -47,23 +43,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
// Get router variables
|
|
||||||
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const path = router.asPath.split("/");
|
|
||||||
path.pop(); // Remove the last item (where we currently are)
|
|
||||||
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
|
|
||||||
const slug = path.join("/");
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col justify-end">
|
|
||||||
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
|
||||||
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
|
|
||||||
<p className="sr-only">Go Back</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSlots = ({
|
const useSlots = ({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
eventTypeSlug,
|
eventTypeSlug,
|
||||||
@@ -71,6 +50,7 @@ const useSlots = ({
|
|||||||
endTime,
|
endTime,
|
||||||
usernameList,
|
usernameList,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
}: {
|
}: {
|
||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
eventTypeSlug: string;
|
eventTypeSlug: string;
|
||||||
@@ -78,6 +58,7 @@ const useSlots = ({
|
|||||||
endTime?: Dayjs;
|
endTime?: Dayjs;
|
||||||
usernameList: string[];
|
usernameList: string[];
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
|
duration?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
||||||
{
|
{
|
||||||
@@ -87,6 +68,7 @@ const useSlots = ({
|
|||||||
startTime: startTime?.toISOString() || "",
|
startTime: startTime?.toISOString() || "",
|
||||||
endTime: endTime?.toISOString() || "",
|
endTime: endTime?.toISOString() || "",
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!startTime && !!endTime,
|
enabled: !!startTime && !!endTime,
|
||||||
@@ -127,6 +109,7 @@ const SlotPicker = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
|
const { duration } = useRouterQuery("duration");
|
||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -152,7 +135,7 @@ const SlotPicker = ({
|
|||||||
setSelectedDate(dayjs.tz(date, timeZone));
|
setSelectedDate(dayjs.tz(date, timeZone));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [router.isReady, month, date, timeZone]);
|
}, [router.isReady, month, date, duration, timeZone]);
|
||||||
|
|
||||||
const { i18n, isLocaleReady } = useLocale();
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
const { slots: _1 } = useSlots({
|
const { slots: _1 } = useSlots({
|
||||||
@@ -162,14 +145,19 @@ const SlotPicker = ({
|
|||||||
startTime: selectedDate?.startOf("day"),
|
startTime: selectedDate?.startOf("day"),
|
||||||
endTime: selectedDate?.endOf("day"),
|
endTime: selectedDate?.endOf("day"),
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
});
|
});
|
||||||
const { slots: _2, isLoading } = useSlots({
|
const { slots: _2, isLoading } = useSlots({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
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"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
||||||
@@ -230,14 +218,14 @@ function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<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 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
<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="text-sm font-medium">
|
<p className="flex items-center text-sm font-medium">
|
||||||
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
||||||
{timeZone}
|
{timeZone}
|
||||||
{isTimeOptionsOpen ? (
|
{isTimeOptionsOpen ? (
|
||||||
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" />
|
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||||
) : (
|
) : (
|
||||||
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
|
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
@@ -259,23 +247,6 @@ const dateQuerySchema = z.object({
|
|||||||
timeZone: z.string().optional().default(""),
|
timeZone: z.string().optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const useRouterQuery = <T extends string>(name: T) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const existingQueryParams = router.asPath.split("?")[1];
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(existingQueryParams);
|
|
||||||
const query = Object.fromEntries(urlParams);
|
|
||||||
|
|
||||||
const setQuery = (newValue: string | number | null | undefined) => {
|
|
||||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
|
||||||
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
return { [name]: query[name], setQuery } as {
|
|
||||||
[K in T]: string | undefined;
|
|
||||||
} & { setQuery: typeof setQuery };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||||
@@ -286,6 +257,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
@@ -310,9 +282,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// TODO: Improve this;
|
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
|
||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -325,7 +294,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
// get dynamic user list here
|
// get dynamic user list here
|
||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
|
|
||||||
@@ -338,6 +307,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
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.
|
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
|
// Define conditional gates here
|
||||||
const gates = [
|
const gates = [
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||||
@@ -371,7 +342,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex-col md:mx-4 lg:flex",
|
"flex flex-col md:mx-4",
|
||||||
shouldAlignCentrally ? "items-center" : "items-start",
|
shouldAlignCentrally ? "items-center" : "items-start",
|
||||||
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
||||||
)}>
|
)}>
|
||||||
@@ -386,60 +357,55 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="overflow-hidden md:flex">
|
||||||
<div
|
{showEventTypeDetails && (
|
||||||
className={classNames(
|
<div
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
className={classNames(
|
||||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
"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 && (
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
<div className="flex items-start text-sm font-medium">
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
<div className="flex items-start text-sm font-medium">
|
||||||
<div>
|
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<div>
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
</p>
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
<input
|
</p>
|
||||||
type="number"
|
<input
|
||||||
min="1"
|
type="number"
|
||||||
max={eventType.recurringEvent.count}
|
min="1"
|
||||||
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"
|
max={eventType.recurringEvent.count}
|
||||||
defaultValue={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"
|
||||||
onChange={(event) => {
|
defaultValue={eventType.recurringEvent.count}
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
onChange={(event) => {
|
||||||
}}
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
/>
|
}}
|
||||||
<p className="inline">
|
/>
|
||||||
{t("occurrence", {
|
<p className="inline">
|
||||||
count: recurringEventCount,
|
{t("occurrence", {
|
||||||
})}
|
count: recurringEventCount,
|
||||||
</p>
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{stripeAppData.price > 0 && (
|
||||||
{stripeAppData.price > 0 && (
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<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" />
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<IntlProvider locale="en">
|
||||||
<IntlProvider locale="en">
|
<FormattedNumber
|
||||||
<FormattedNumber
|
value={stripeAppData.price / 100.0}
|
||||||
value={stripeAppData.price / 100.0}
|
style="currency"
|
||||||
style="currency"
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
/>
|
||||||
/>
|
</IntlProvider>
|
||||||
</IntlProvider>
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
{timezoneDropdown}
|
||||||
{timezoneDropdown}
|
</BookingDescription>
|
||||||
</BookingDescription>
|
|
||||||
|
|
||||||
{!isEmbed && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div className="mt-auto hidden md:block">
|
|
||||||
<GoBackToPreviousPage t={t} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
||||||
@@ -452,7 +418,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)*/}
|
)*/}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<SlotPicker
|
<SlotPicker
|
||||||
weekStart={
|
weekStart={
|
||||||
typeof profile.weekStart === "string"
|
typeof profile.weekStart === "string"
|
||||||
@@ -478,7 +445,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useState, useReducer } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { ReactMultiEmail } from "react-multi-email";
|
import { ReactMultiEmail } from "react-multi-email";
|
||||||
@@ -14,11 +14,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import {
|
import {
|
||||||
locationKeyToString,
|
|
||||||
getEventLocationValue,
|
|
||||||
getEventLocationType,
|
|
||||||
EventLocationType,
|
EventLocationType,
|
||||||
|
getEventLocationType,
|
||||||
|
getEventLocationValue,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
|
locationKeyToString,
|
||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
@@ -26,28 +26,26 @@ import { LocationObject, LocationType } from "@calcom/core/location";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Group, RadioField } from "@calcom/ui";
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import AddressInput from "@calcom/ui/form/AddressInputLazy";
|
|
||||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
|
||||||
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { ensureArray } from "@lib/ensureArray";
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
@@ -90,10 +88,9 @@ const BookingPage = ({
|
|||||||
...restProps
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
// Get user so we can determine 12/24 hour format preferences
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
const query = useMeQuery();
|
|
||||||
const user = query.data;
|
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -108,6 +105,11 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const stripeAppData = getStripeAppData(eventType);
|
||||||
|
// Define duration now that we support multiple duration eventTypes
|
||||||
|
let duration = eventType.length;
|
||||||
|
if (queryDuration && !isNaN(Number(queryDuration))) {
|
||||||
|
duration = Number(queryDuration);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
@@ -136,12 +138,12 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid,
|
|
||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -152,12 +154,12 @@ const BookingPage = ({
|
|||||||
const { uid } = responseData[0] || {};
|
const { uid } = responseData[0] || {};
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid,
|
|
||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -242,7 +244,7 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().trim().email(),
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
@@ -299,6 +301,39 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bookEvent = (booking: BookingFormValues) => {
|
const bookEvent = (booking: BookingFormValues) => {
|
||||||
|
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(
|
telemetry.event(
|
||||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||||
{ isTeamBooking: document.URL.includes("team/") }
|
{ isTeamBooking: document.URL.includes("team/") }
|
||||||
@@ -317,13 +352,29 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (recurringDates.length) {
|
if (recurringDates.length) {
|
||||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||||
...booking,
|
...booking,
|
||||||
start: dayjs(recurringDate).format(),
|
start: dayjs(recurringDate).format(),
|
||||||
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
|
end: dayjs(recurringDate).add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
recurringEventId,
|
recurringEventId,
|
||||||
@@ -339,10 +390,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -356,7 +404,7 @@ const BookingPage = ({
|
|||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...booking,
|
...booking,
|
||||||
start: dayjs(date).format(),
|
start: dayjs(date).format(),
|
||||||
end: dayjs(date).add(eventType.length, "minute").format(),
|
end: dayjs(date).add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
timeZone: timeZone(),
|
timeZone: timeZone(),
|
||||||
@@ -370,10 +418,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -408,6 +453,7 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
@@ -431,7 +477,7 @@ const BookingPage = ({
|
|||||||
eventTypeTitle: eventType.title,
|
eventTypeTitle: eventType.title,
|
||||||
profileName: profile.name,
|
profileName: profile.name,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
| Cal.com
|
| {APP_NAME}
|
||||||
</title>
|
</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
@@ -441,7 +487,7 @@ const BookingPage = ({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? "" : "sm:my-24",
|
isEmbed ? "" : "sm:my-24",
|
||||||
"my-0 max-w-3xl "
|
"my-0 max-w-3xl"
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -450,95 +496,97 @@ const BookingPage = ({
|
|||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"dark:border-darkgray-300 rounded-md sm:border"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<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">
|
{showEventTypeDetails && (
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<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">
|
||||||
{stripeAppData.price > 0 && (
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
{stripeAppData.price > 0 && (
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<IntlProvider locale="en">
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<FormattedNumber
|
<IntlProvider locale="en">
|
||||||
value={stripeAppData.price / 100.0}
|
<FormattedNumber
|
||||||
style="currency"
|
value={stripeAppData.price / 100.0}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
style="currency"
|
||||||
/>
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
</IntlProvider>
|
/>
|
||||||
</p>
|
</IntlProvider>
|
||||||
)}
|
|
||||||
{!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,
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||||
<div className="text-sm font-medium">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
{getEveryFreqFor({
|
||||||
{!rescheduleUid &&
|
t,
|
||||||
eventType.recurringEvent?.freq &&
|
recurringEvent: eventType.recurringEvent,
|
||||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
recurringCount: recurringEventCount,
|
||||||
return <p key={key}>{timeFormatted}</p>;
|
})}
|
||||||
})}
|
</p>
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
</div>
|
||||||
<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 && (
|
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<Icon.FiUser
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||||
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
<div className="text-sm font-medium">
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||||
? "text-rose-600"
|
{!rescheduleUid &&
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
eventType.recurringEvent?.freq &&
|
||||||
? "text-yellow-500"
|
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||||
: "text-bookinghighlight"
|
return <p key={key}>{timeFormatted}</p>;
|
||||||
}`}
|
})}
|
||||||
/>
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
<p
|
<div className="flex">
|
||||||
className={`${
|
<Tooltip
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
? "text-rose-600"
|
<p key={key}>{timeFormatted}</p>
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
))}>
|
||||||
? "text-yellow-500"
|
<p className="dark:text-darkgray-600 text-sm">
|
||||||
: "text-bookinghighlight"
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
} mb-2 font-medium`}>
|
</p>
|
||||||
{booking
|
</Tooltip>
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
</div>
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
)}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{booking?.startTime && rescheduleUid && (
|
||||||
</BookingDescription>
|
<div>
|
||||||
</div>
|
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||||
<div className="p-6 sm:w-1/2">
|
{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 && (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BookingDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
|
||||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
@@ -627,7 +675,7 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
{/* TODO: Change name and id ="phone" to something generic */}
|
{/* TODO: Change name and id ="phone" to something generic */}
|
||||||
{AttendeeInput && (
|
{AttendeeInput && !disableInput && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor={
|
htmlFor={
|
||||||
@@ -664,7 +712,6 @@ const BookingPage = ({
|
|||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
disabled={disableInput}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.phone && (
|
{bookingForm.formState.errors.phone && (
|
||||||
@@ -682,8 +729,11 @@ const BookingPage = ({
|
|||||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||||
<label
|
<label
|
||||||
htmlFor={"custom_" + input.id}
|
htmlFor={"custom_" + input.id}
|
||||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
className={classNames(
|
||||||
{input.label}
|
"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>
|
</label>
|
||||||
)}
|
)}
|
||||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||||
@@ -747,6 +797,48 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
||||||
|
<div className="flex">
|
||||||
|
<Group
|
||||||
|
required={input.required}
|
||||||
|
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`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
{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 ">
|
||||||
|
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||||
|
<p>{t("invalid_number")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!eventType.disableGuests && guestToggle && (
|
{!eventType.disableGuests && guestToggle && (
|
||||||
@@ -845,7 +937,7 @@ const BookingPage = ({
|
|||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
{...bookingForm.register("notes")}
|
{...bookingForm.register("notes")}
|
||||||
required={!!eventType.metadata.additionalNotesRequired}
|
required={!!eventType.metadata?.additionalNotesRequired}
|
||||||
id="notes"
|
id="notes"
|
||||||
name="notes"
|
name="notes"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -860,7 +952,7 @@ const BookingPage = ({
|
|||||||
{!eventType.disableGuests && !guestToggle && (
|
{!eventType.disableGuests && !guestToggle && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="minimalSecondary"
|
color="minimal"
|
||||||
size="icon"
|
size="icon"
|
||||||
tooltip={t("additional_guests")}
|
tooltip={t("additional_guests")}
|
||||||
StartIcon={Icon.FiUserPlus}
|
StartIcon={Icon.FiUserPlus}
|
||||||
@@ -878,7 +970,6 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent"
|
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
@@ -911,7 +1002,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
|||||||
<div className="ltr:ml-3 rtl:mr-3">
|
<div className="ltr:ml-3 rtl:mr-3">
|
||||||
<p className="text-sm text-yellow-700">
|
<p className="text-sm text-yellow-700">
|
||||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||||
{error instanceof HttpError || error instanceof Error ? error.message : "Unknown error"}
|
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
||||||
import React, { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button } from "@calcom/ui/Button";
|
|
||||||
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
|
|
||||||
export type DeleteStripeDialogContentProps = {
|
|
||||||
cancelAllBookingsBtnText?: string;
|
|
||||||
removeBtnText?: string;
|
|
||||||
cancelBtnText?: string;
|
|
||||||
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
||||||
onRemove?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
||||||
title: string;
|
|
||||||
variety?: "danger" | "warning" | "success";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DeleteStripeDialogContent(props: PropsWithChildren<DeleteStripeDialogContentProps>) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
variety,
|
|
||||||
cancelAllBookingsBtnText,
|
|
||||||
removeBtnText,
|
|
||||||
cancelBtnText = t("cancel"),
|
|
||||||
onConfirm,
|
|
||||||
onRemove,
|
|
||||||
children,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent>
|
|
||||||
<div className="flex">
|
|
||||||
{variety && (
|
|
||||||
<div className="mt-0.5 ltr:mr-3">
|
|
||||||
{variety === "danger" && (
|
|
||||||
<div className="mx-auto rounded-full bg-red-100 p-2 text-center">
|
|
||||||
<Icon.FiAlertTriangle className="h-5 w-5 text-red-600" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{variety === "warning" && (
|
|
||||||
<div className="mx-auto rounded-full bg-orange-100 p-2 text-center">
|
|
||||||
<Icon.FiAlertTriangle className="h-5 w-5 text-orange-600" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{variety === "success" && (
|
|
||||||
<div className="mx-auto rounded-full bg-green-100 p-2 text-center">
|
|
||||||
<Icon.FiCheck className="h-5 w-5 text-green-600" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<DialogPrimitive.Title className="font-cal text-xl text-gray-900">{title}</DialogPrimitive.Title>
|
|
||||||
<DialogPrimitive.Description className="text-sm text-neutral-500">
|
|
||||||
{children}
|
|
||||||
</DialogPrimitive.Description>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-8">
|
|
||||||
<DialogClose onClick={onConfirm} asChild>
|
|
||||||
<Button color="alert">{cancelAllBookingsBtnText}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose onClick={onRemove} asChild>
|
|
||||||
<Button color="alert2">{removeBtnText}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">{cancelBtnText}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,23 +3,21 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { components } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LocationType,
|
|
||||||
getEventLocationType,
|
|
||||||
EventLocationType,
|
EventLocationType,
|
||||||
|
getEventLocationType,
|
||||||
|
getHumanReadableLocationValue,
|
||||||
|
getMessageForOrganizer,
|
||||||
LocationObject,
|
LocationObject,
|
||||||
|
LocationType,
|
||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { getMessageForOrganizer } from "@calcom/app-store/locations";
|
import { classNames } from "@calcom/lib";
|
||||||
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -42,6 +40,7 @@ interface ISetLocationDialog {
|
|||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
||||||
|
setEditingLocationType?: (param: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationInput = (props: {
|
const LocationInput = (props: {
|
||||||
@@ -79,6 +78,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
setSelectedLocation,
|
setSelectedLocation,
|
||||||
|
setEditingLocationType,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
||||||
@@ -212,13 +212,13 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog}>
|
<Dialog open={isOpenDialog}>
|
||||||
<DialogContent asChild>
|
<DialogContent>
|
||||||
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<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">
|
||||||
<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" />
|
||||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
</div>
|
||||||
</div>
|
<div className="w-full">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
{t("edit_location")}
|
{t("edit_location")}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -226,118 +226,152 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" />
|
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
||||||
</div>
|
|
||||||
{booking && (
|
|
||||||
<>
|
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Form
|
|
||||||
form={locationFormMethods}
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
|
||||||
|
|
||||||
let details = {};
|
{booking && (
|
||||||
if (newLocation === LocationType.InPerson) {
|
<>
|
||||||
details = {
|
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
||||||
address: values.locationAddress,
|
<p className="mb-2 ml-1 text-sm text-black">
|
||||||
};
|
{getHumanReadableLocationValue(booking.location, t)}
|
||||||
}
|
</p>
|
||||||
const eventLocationType = getEventLocationType(newLocation);
|
</>
|
||||||
|
)}
|
||||||
|
<Form
|
||||||
|
form={locationFormMethods}
|
||||||
|
handleSubmit={async (values) => {
|
||||||
|
const { locationType: newLocation, displayLocationPublicly } = values;
|
||||||
|
|
||||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
let details = {};
|
||||||
if (
|
if (newLocation === LocationType.InPerson) {
|
||||||
newLocation === LocationType.Link ||
|
details = {
|
||||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
address: values.locationAddress,
|
||||||
) {
|
};
|
||||||
details = { link: values.locationLink };
|
}
|
||||||
}
|
const eventLocationType = getEventLocationType(newLocation);
|
||||||
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
// TODO: There can be a property that tells if it is to be saved in `link`
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
if (
|
||||||
}
|
newLocation === LocationType.Link ||
|
||||||
|
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
||||||
|
) {
|
||||||
|
details = { link: values.locationLink };
|
||||||
|
}
|
||||||
|
|
||||||
if (eventLocationType?.organizerInputType) {
|
if (newLocation === LocationType.UserPhone) {
|
||||||
details = {
|
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||||
...details,
|
}
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLocation(newLocation, details);
|
if (eventLocationType?.organizerInputType) {
|
||||||
setShowLocationModal(false);
|
details = {
|
||||||
setSelectedLocation?.(undefined);
|
...details,
|
||||||
locationFormMethods.unregister([
|
displayLocationPublicly,
|
||||||
"locationType",
|
};
|
||||||
"locationLink",
|
}
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
saveLocation(newLocation, details);
|
||||||
]);
|
setShowLocationModal(false);
|
||||||
}}>
|
setSelectedLocation?.(undefined);
|
||||||
<QueryCell
|
locationFormMethods.unregister([
|
||||||
query={locationsQuery}
|
"locationType",
|
||||||
success={({ data: locationOptions }) => {
|
"locationLink",
|
||||||
if (!locationOptions.length) return null;
|
"locationAddress",
|
||||||
return (
|
"locationPhoneNumber",
|
||||||
<Controller
|
]);
|
||||||
name="locationType"
|
}}>
|
||||||
control={locationFormMethods.control}
|
<QueryCell
|
||||||
render={() => (
|
query={locationsQuery}
|
||||||
<Select
|
success={({ data: locationOptions }) => {
|
||||||
maxMenuHeight={150}
|
if (!locationOptions.length) return null;
|
||||||
name="location"
|
if (booking) {
|
||||||
defaultValue={selection}
|
locationOptions.forEach((location) => {
|
||||||
options={
|
if (location.label === "phone") {
|
||||||
booking
|
location.options.filter((l) => l.value !== "phone");
|
||||||
? locationOptions.filter(
|
} else if (location.label === "in person") {
|
||||||
(location) =>
|
location.options.filter((l) => l.value !== "attendeeInPerson");
|
||||||
location.value !== "phone" && location.value !== "attendeeInPerson"
|
}
|
||||||
)
|
});
|
||||||
: locationOptions
|
}
|
||||||
}
|
return (
|
||||||
isSearchable
|
<Controller
|
||||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
name="locationType"
|
||||||
onChange={(val) => {
|
control={locationFormMethods.control}
|
||||||
if (val) {
|
render={() => (
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
<Select<{ label: string; value: string; icon?: string }>
|
||||||
locationFormMethods.unregister([
|
maxMenuHeight={300}
|
||||||
"locationLink",
|
name="location"
|
||||||
"locationAddress",
|
defaultValue={selection}
|
||||||
"locationPhoneNumber",
|
options={locationOptions}
|
||||||
]);
|
components={{
|
||||||
locationFormMethods.clearErrors([
|
Option: (props) => (
|
||||||
"locationLink",
|
<components.Option {...props}>
|
||||||
"locationPhoneNumber",
|
<div className="flex items-center gap-3">
|
||||||
"locationAddress",
|
{props.data.icon && (
|
||||||
]);
|
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
|
||||||
setSelectedLocation?.(val);
|
)}
|
||||||
}
|
<span
|
||||||
}}
|
className={classNames(
|
||||||
/>
|
"text-sm font-medium",
|
||||||
)}
|
props.isSelected ? "text-white" : "text-gray-900"
|
||||||
/>
|
)}>
|
||||||
);
|
{props.data.label}
|
||||||
}}
|
</span>
|
||||||
/>
|
</div>
|
||||||
{selectedLocation && LocationOptions}
|
</components.Option>
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
),
|
||||||
<Button
|
}}
|
||||||
onClick={() => {
|
formatOptionLabel={(e) => (
|
||||||
setShowLocationModal(false);
|
<div className="flex items-center gap-3">
|
||||||
setSelectedLocation?.(undefined);
|
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
|
||||||
locationFormMethods.unregister("locationType");
|
<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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
type="button"
|
/>
|
||||||
color="secondary">
|
{selectedLocation && LocationOptions}
|
||||||
{t("cancel")}
|
<DialogFooter>
|
||||||
</Button>
|
<div className="mt-4 flex justify-end space-x-2">
|
||||||
<Button type="submit">{t("update")}</Button>
|
<Button
|
||||||
</div>
|
onClick={() => {
|
||||||
</Form>
|
setShowLocationModal(false);
|
||||||
|
setSelectedLocation?.(undefined);
|
||||||
|
setEditingLocationType?.("");
|
||||||
|
locationFormMethods.unregister("locationType");
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
color="secondary">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit">{t("update")}</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import React, { useState, Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import {
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
Button,
|
||||||
import { Button } from "@calcom/ui/components/button";
|
Dialog,
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
DialogClose,
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
Icon,
|
||||||
|
showToast,
|
||||||
|
TextArea,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -41,8 +47,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("send_reschedule_request")} />
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
|
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
|
||||||
<p className="-mt-8 text-sm text-gray-500">{t("reschedule_modal_description")}</p>
|
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
||||||
{t("reason_for_reschedule_request")}
|
{t("reason_for_reschedule_request")}
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||||
@@ -56,9 +61,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose />
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
<Button
|
||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FormValues } from "pages/event-types/[type]";
|
import { FormValues } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { SingleValueProps, OptionProps, components } from "react-select";
|
import { components, OptionProps, SingleValueProps } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
@@ -8,12 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
import { SettingsToggle } from "@calcom/ui/v2";
|
|
||||||
import Select from "@calcom/ui/v2/core/form/select";
|
|
||||||
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
@@ -25,12 +20,13 @@ type AvailabilityOption = {
|
|||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.Option>
|
</components.Option>
|
||||||
@@ -39,12 +35,13 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.SingleValue>
|
</components.SingleValue>
|
||||||
@@ -62,6 +59,8 @@ const AvailabilitySelect = ({
|
|||||||
onChange: (value: AvailabilityOption | null) => void;
|
onChange: (value: AvailabilityOption | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <SelectSkeletonLoader />;
|
return <SelectSkeletonLoader />;
|
||||||
}
|
}
|
||||||
@@ -82,6 +81,7 @@ const AvailabilitySelect = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
|||||||
@@ -1,346 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
|
||||||
import { Button } from "@calcom/ui/Button";
|
|
||||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import Dropdown, {
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@calcom/ui/Dropdown";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import { slugify } from "@lib/slugify";
|
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
|
||||||
import * as RadioArea from "@components/ui/form/radio-area";
|
|
||||||
|
|
||||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
|
||||||
export interface EventTypeParent {
|
|
||||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
|
||||||
name?: string | null;
|
|
||||||
slug?: string | null;
|
|
||||||
image?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateEventTypeBtnProps {
|
|
||||||
// set true for use on the team settings page
|
|
||||||
canAddEvents: boolean;
|
|
||||||
// set true when in use on the team settings page
|
|
||||||
isIndividualTeam?: boolean;
|
|
||||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
|
||||||
options: EventTypeParent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|
||||||
const { t, isLocaleReady } = useLocale();
|
|
||||||
const router = useRouter();
|
|
||||||
// URL encoded params
|
|
||||||
const teamId: number | undefined =
|
|
||||||
typeof router.query.teamId === "string" && router.query.teamId
|
|
||||||
? parseInt(router.query.teamId)
|
|
||||||
: undefined;
|
|
||||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
|
||||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
|
||||||
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
|
||||||
resolver: zodResolver(createEventTypeInput),
|
|
||||||
});
|
|
||||||
const { setValue, watch, register } = form;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) return;
|
|
||||||
|
|
||||||
const title: string =
|
|
||||||
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
|
|
||||||
const length: number =
|
|
||||||
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
|
|
||||||
const description: string =
|
|
||||||
typeof router.query.description === "string" && router.query.description
|
|
||||||
? router.query.description
|
|
||||||
: "";
|
|
||||||
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
|
|
||||||
|
|
||||||
setValue("title", title);
|
|
||||||
setValue("length", length);
|
|
||||||
setValue("description", description);
|
|
||||||
setValue("slug", slug);
|
|
||||||
// If query params change, update the form
|
|
||||||
}, [router.isReady, router.query, setValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const subscription = watch((value, { name, type }) => {
|
|
||||||
if (name === "title" && type === "change") {
|
|
||||||
if (value.title) setValue("slug", slugify(value.title));
|
|
||||||
else setValue("slug", "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}, [watch, setValue]);
|
|
||||||
|
|
||||||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
|
||||||
onSuccess: async ({ eventType }) => {
|
|
||||||
await router.replace("/event-types/" + eventType.id);
|
|
||||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
if (err instanceof HttpError) {
|
|
||||||
const message = `${err.statusCode}: ${err.message}`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.data?.code === "BAD_REQUEST") {
|
|
||||||
const message = `${err.data.code}: URL already exists.`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.data?.code === "UNAUTHORIZED") {
|
|
||||||
const message = `${err.data.code}: You are not able to create this event`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// inject selection data into url for correct router history
|
|
||||||
const openModal = (option: EventTypeParent) => {
|
|
||||||
const query = {
|
|
||||||
...router.query,
|
|
||||||
dialog: "new-eventtype",
|
|
||||||
eventPage: option.slug,
|
|
||||||
teamId: option.teamId,
|
|
||||||
};
|
|
||||||
if (!option.teamId) {
|
|
||||||
delete query.teamId;
|
|
||||||
}
|
|
||||||
router.push(
|
|
||||||
{
|
|
||||||
pathname: router.pathname,
|
|
||||||
query,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ shallow: true }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (!isLocaleReady) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name="new-eventtype"
|
|
||||||
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
|
|
||||||
<CreateEventTypeTrigger
|
|
||||||
hasTeams={hasTeams}
|
|
||||||
canAddEvents={props.canAddEvents}
|
|
||||||
isIndividualTeam={props.isIndividualTeam}
|
|
||||||
openModal={openModal}
|
|
||||||
options={props.options}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
|
||||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
handleSubmit={(values) => {
|
|
||||||
createMutation.mutate(values);
|
|
||||||
}}>
|
|
||||||
<div className="mt-3 space-y-4">
|
|
||||||
{teamId && (
|
|
||||||
<TextField
|
|
||||||
type="hidden"
|
|
||||||
labelProps={{ style: { display: "none" } }}
|
|
||||||
{...register("teamId", { valueAsNumber: true })}
|
|
||||||
value={teamId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
|
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
|
||||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
|
||||||
<TextField
|
|
||||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
|
||||||
required
|
|
||||||
addOnLeading={<InputLeading>/{pageSlug}/</InputLeading>}
|
|
||||||
{...register("slug")}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextField
|
|
||||||
label={t("url")}
|
|
||||||
required
|
|
||||||
addOnLeading={
|
|
||||||
<InputLeading>
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
|
||||||
</InputLeading>
|
|
||||||
}
|
|
||||||
{...register("slug")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextAreaField
|
|
||||||
label={t("description")}
|
|
||||||
placeholder={t("quick_video_meeting")}
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="10"
|
|
||||||
placeholder="15"
|
|
||||||
label={t("length")}
|
|
||||||
className="pr-20"
|
|
||||||
{...register("length", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 mt-1.5 flex items-center pt-4 pr-3 text-sm text-gray-400">
|
|
||||||
{t("minutes")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{teamId && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
|
||||||
{t("scheduling_type")}
|
|
||||||
</label>
|
|
||||||
{form.formState.errors.schedulingType && (
|
|
||||||
<Alert
|
|
||||||
className="mt-1"
|
|
||||||
severity="error"
|
|
||||||
message={form.formState.errors.schedulingType.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<RadioArea.Group
|
|
||||||
{...register("schedulingType")}
|
|
||||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
|
||||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
|
||||||
<RadioArea.Item
|
|
||||||
value={SchedulingType.COLLECTIVE}
|
|
||||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
|
||||||
className="w-1/2 text-sm">
|
|
||||||
<strong className="mb-1 block">{t("collective")}</strong>
|
|
||||||
<p>{t("collective_description")}</p>
|
|
||||||
</RadioArea.Item>
|
|
||||||
<RadioArea.Item
|
|
||||||
value={SchedulingType.ROUND_ROBIN}
|
|
||||||
defaultChecked={type === SchedulingType.ROUND_ROBIN}
|
|
||||||
className="w-1/2 text-sm">
|
|
||||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
|
||||||
<p>{t("round_robin_description")}</p>
|
|
||||||
</RadioArea.Item>
|
|
||||||
</RadioArea.Group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
|
||||||
<Button type="submit" loading={createMutation.isLoading}>
|
|
||||||
{t("continue")}
|
|
||||||
</Button>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateEventTypeTrigger = {
|
|
||||||
isIndividualTeam?: boolean;
|
|
||||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
|
||||||
options: EventTypeParent[];
|
|
||||||
hasTeams: boolean;
|
|
||||||
// set true for use on the team settings page
|
|
||||||
canAddEvents: boolean;
|
|
||||||
openModal: (option: EventTypeParent) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!props.hasTeams || props.isIndividualTeam ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => props.openModal(props.options[0])}
|
|
||||||
data-testid="new-event-type"
|
|
||||||
StartIcon={Icon.FiPlus}
|
|
||||||
disabled={!props.canAddEvents}>
|
|
||||||
{t("new_event_type_btn")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
|
||||||
{props.options.map((option) => (
|
|
||||||
<CreateEventTeamsItem
|
|
||||||
key={option.slug}
|
|
||||||
option={option}
|
|
||||||
openModal={() => props.openModal(option)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateEventTeamsItem(props: {
|
|
||||||
openModal: (option: EventTypeParent) => void;
|
|
||||||
option: EventTypeParent;
|
|
||||||
}) {
|
|
||||||
const session = useSession();
|
|
||||||
const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({
|
|
||||||
memberId: session.data?.user.id as number,
|
|
||||||
teamId: props.option.teamId as number,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDisabled = membershipQuery.data?.role === "MEMBER";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={props.option.slug}
|
|
||||||
className={classNames(
|
|
||||||
"cursor-pointer px-3 py-2 focus:outline-none",
|
|
||||||
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
|
|
||||||
)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onSelect={() => props.openModal(props.option)}>
|
|
||||||
<Avatar
|
|
||||||
alt={props.option.name || ""}
|
|
||||||
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
|
|
||||||
size={6}
|
|
||||||
className="inline ltr:mr-2 rtl:ml-2"
|
|
||||||
/>
|
|
||||||
{props.option.name ? props.option.name : props.option.slug}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import React, { FC } from "react";
|
import { EventTypeCustomInputType } from "@prisma/client";
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { Button } from "@calcom/ui/components";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TextField } from "@calcom/ui/components/form";
|
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
||||||
import { Select } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
interface OptionTypeBase {
|
||||||
label: string;
|
label: string;
|
||||||
value: EventTypeCustomInputType;
|
value: EventTypeCustomInputType;
|
||||||
|
options?: { label: string; type: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
onSubmit: (output: CustomInputParsed) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
selectedCustomInput?: CustomInputParsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
type IFormInput = CustomInputParsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting a random ID gives us the option to know WHICH field is changed
|
||||||
|
* when the user edits a custom field.
|
||||||
|
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
|
||||||
|
* There is very very very slim chance that this will cause a collision
|
||||||
|
* */
|
||||||
|
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -28,10 +36,23 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
||||||
|
{
|
||||||
|
value: EventTypeCustomInputType.RADIO,
|
||||||
|
label: t("radio"),
|
||||||
|
},
|
||||||
|
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
const { selectedCustomInput } = props;
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
const defaultValues = selectedCustomInput
|
||||||
|
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
|
||||||
|
: {
|
||||||
|
id: randomId(),
|
||||||
|
type: EventTypeCustomInputType.TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { register, control, getValues } = useForm<IFormInput>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
const selectedInputType = useWatch({ name: "type", control });
|
||||||
@@ -42,7 +63,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(props.onSubmit)} className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
||||||
{t("input_type")}
|
{t("input_type")}
|
||||||
@@ -85,6 +106,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{...register("placeholder")}
|
{...register("placeholder")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedInputType === EventTypeCustomInputType.RADIO && (
|
||||||
|
<RadioInputHandler control={control} register={register} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
<input
|
<input
|
||||||
id="required"
|
id="required"
|
||||||
@@ -113,10 +138,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">{t("save")}</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onSubmit(getValues());
|
||||||
|
}}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function RadioInputHandler({
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
}: {
|
||||||
|
register: UseFormRegister<IFormInput>;
|
||||||
|
control: Control<IFormInput>;
|
||||||
|
}) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { fields, append, remove } = useFieldArray<IFormInput>({
|
||||||
|
control,
|
||||||
|
name: "options",
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col ">
|
||||||
|
<Label htmlFor="radio_options">{t("options")}</Label>
|
||||||
|
<ul
|
||||||
|
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
|
||||||
|
ref={animateRef}>
|
||||||
|
<>
|
||||||
|
{fields.map((option, index) => (
|
||||||
|
<li key={`${option.id}`}>
|
||||||
|
<TextField
|
||||||
|
id={option.id}
|
||||||
|
placeholder={t("enter_option", { index: index + 1 })}
|
||||||
|
addOnFilled={false}
|
||||||
|
label={t("option", { index: index + 1 })}
|
||||||
|
labelSrOnly
|
||||||
|
{...register(`options.${index}.label` as const, { required: true })}
|
||||||
|
addOnSuffix={
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiX}
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
className="!text-sm !font-medium"
|
||||||
|
onClick={() => {
|
||||||
|
append({ label: "", type: "text" });
|
||||||
|
}}>
|
||||||
|
{t("add_an_option")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
export default CustomInputTypeForm;
|
||||||
|
|||||||
@@ -1,347 +0,0 @@
|
|||||||
import { ErrorMessage } from "@hookform/error-message";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import {
|
|
||||||
LocationType,
|
|
||||||
getEventLocationType,
|
|
||||||
EventLocationType,
|
|
||||||
getHumanReadableLocationValue,
|
|
||||||
getMessageForOrganizer,
|
|
||||||
LocationObject,
|
|
||||||
} from "@calcom/app-store/locations";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import { Label, Form } from "@calcom/ui/components/form";
|
|
||||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/v2";
|
|
||||||
import { Select } from "@calcom/ui/v2/";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
import { LinkText } from "@components/ui/LinkText";
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
|
||||||
|
|
||||||
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;
|
|
||||||
booking?: BookingItem;
|
|
||||||
defaultValues?: LocationObject[];
|
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isOpenDialog: boolean;
|
|
||||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationInput = (props: {
|
|
||||||
eventLocationType: EventLocationType;
|
|
||||||
locationFormMethods: ReturnType<typeof useForm>;
|
|
||||||
id: string;
|
|
||||||
required: boolean;
|
|
||||||
placeholder: string;
|
|
||||||
className?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
}): JSX.Element | null => {
|
|
||||||
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
|
||||||
return (
|
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
|
||||||
);
|
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
|
||||||
return (
|
|
||||||
<PhoneInput
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
{...remainingProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|
||||||
const {
|
|
||||||
saveLocation,
|
|
||||||
selection,
|
|
||||||
booking,
|
|
||||||
setShowLocationModal,
|
|
||||||
isOpenDialog,
|
|
||||||
defaultValues,
|
|
||||||
setSelectedLocation,
|
|
||||||
} = props;
|
|
||||||
const { t } = useLocale();
|
|
||||||
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selection) {
|
|
||||||
locationFormMethods.setValue("locationType", selection?.value);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selection]);
|
|
||||||
|
|
||||||
const locationFormSchema = z.object({
|
|
||||||
locationType: z.string(),
|
|
||||||
phone: z.string().optional(),
|
|
||||||
locationAddress: z.string().optional(),
|
|
||||||
locationLink: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.superRefine((val, ctx) => {
|
|
||||||
if (
|
|
||||||
eventLocationType &&
|
|
||||||
!eventLocationType.default &&
|
|
||||||
eventLocationType.linkType === "static" &&
|
|
||||||
eventLocationType.urlRegExp
|
|
||||||
) {
|
|
||||||
const valid = z
|
|
||||||
.string()
|
|
||||||
.regex(new RegExp(eventLocationType.urlRegExp || ""))
|
|
||||||
.safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL for ${eventLocationType.label}. ${
|
|
||||||
sampleUrl ? "Sample URL: " + sampleUrl : ""
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = z.string().url().optional().safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
displayLocationPublicly: z.boolean().optional(),
|
|
||||||
locationPhoneNumber: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const locationFormMethods = useForm({
|
|
||||||
mode: "onSubmit",
|
|
||||||
resolver: zodResolver(locationFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedLocation = useWatch({
|
|
||||||
control: locationFormMethods.control,
|
|
||||||
name: "locationType",
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventLocationType = getEventLocationType(selectedLocation);
|
|
||||||
|
|
||||||
const defaultLocation = defaultValues?.find(
|
|
||||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
|
||||||
);
|
|
||||||
|
|
||||||
const LocationOptions = (() => {
|
|
||||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
|
||||||
if (!eventLocationType.variable) {
|
|
||||||
console.error("eventLocationType.variable can't be undefined");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
|
|
||||||
<LocationInput
|
|
||||||
locationFormMethods={locationFormMethods}
|
|
||||||
eventLocationType={eventLocationType}
|
|
||||||
id="locationInput"
|
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
|
||||||
required
|
|
||||||
className="block w-full rounded-md border-gray-300 text-sm"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
defaultValue={
|
|
||||||
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ErrorMessage
|
|
||||||
errors={locationFormMethods.formState.errors}
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
className="mt-1 text-sm text-red-500"
|
|
||||||
as="p"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpenDialog}>
|
|
||||||
<DialogContent
|
|
||||||
type="creation"
|
|
||||||
Icon={Icon.FiMapPin}
|
|
||||||
useOwnActionButtons
|
|
||||||
title={t("edit_location")}
|
|
||||||
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
|
|
||||||
{booking && (
|
|
||||||
<>
|
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Form
|
|
||||||
form={locationFormMethods}
|
|
||||||
className="space-y-4"
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
|
||||||
|
|
||||||
let details = {};
|
|
||||||
if (newLocation === LocationType.AttendeeInPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.InPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const eventLocationType = getEventLocationType(newLocation);
|
|
||||||
|
|
||||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
|
||||||
if (
|
|
||||||
newLocation === LocationType.Link ||
|
|
||||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
|
||||||
) {
|
|
||||||
details = { link: values.locationLink };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLocationType?.organizerInputType) {
|
|
||||||
details = {
|
|
||||||
...details,
|
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLocation(newLocation, details);
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationType",
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
}}>
|
|
||||||
<QueryCell
|
|
||||||
query={locationsQuery}
|
|
||||||
success={({ data: locationOptions }) => {
|
|
||||||
if (!locationOptions.length) return null;
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name="locationType"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<Select
|
|
||||||
maxMenuHeight={150}
|
|
||||||
name="location"
|
|
||||||
defaultValue={selection}
|
|
||||||
options={
|
|
||||||
booking
|
|
||||||
? locationOptions.filter((location) => location.value !== "phone")
|
|
||||||
: locationOptions
|
|
||||||
}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) {
|
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
locationFormMethods.clearErrors([
|
|
||||||
"locationLink",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
"locationAddress",
|
|
||||||
]);
|
|
||||||
setSelectedLocation?.(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{selectedLocation && LocationOptions}
|
|
||||||
<div className="mt-5">
|
|
||||||
<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
|
|
||||||
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
|
|
||||||
App Store
|
|
||||||
</LinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister("locationType");
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
color="secondary">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,34 @@
|
|||||||
import { EventTypeCustomInput } from "@prisma/client/";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import {
|
||||||
import { Checkbox, Button, TextField, Label } from "@calcom/ui/components";
|
Button,
|
||||||
import { CustomInputItem, Dialog, DialogContent, SettingsToggle, showToast, Tooltip } from "@calcom/ui/v2";
|
Checkbox,
|
||||||
|
CustomInputItem,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Icon,
|
||||||
|
Label,
|
||||||
|
SettingsToggle,
|
||||||
|
showToast,
|
||||||
|
TextField,
|
||||||
|
Tooltip,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||||
|
|
||||||
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
const generateHashedLink = (id: number) => {
|
const generateHashedLink = (id: number) => {
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const seed = `${id}:${new Date().getTime()}`;
|
const seed = `${id}:${new Date().getTime()}`;
|
||||||
@@ -31,11 +44,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
|
||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
@@ -89,7 +103,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
@@ -98,7 +112,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
StartIcon={Icon.FiEdit}
|
StartIcon={Icon.FiEdit}
|
||||||
size="icon"
|
size="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
onClick={() => setShowEventNameTip((old) => !old)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -113,15 +127,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
if (e && customInputs.length === 0) {
|
if (e && customInputs.length === 0) {
|
||||||
// Push a placeholders
|
// Push a placeholders
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
} else if (!e) {
|
} else if (!e) {
|
||||||
setCustomInputs([]);
|
|
||||||
formMethods.setValue("customInputs", []);
|
formMethods.setValue("customInputs", []);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<ul className="my-4">
|
<ul className="my-4 rounded-md border">
|
||||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
{customInputs.map((customInput, idx) => (
|
||||||
<CustomInputItem
|
<CustomInputItem
|
||||||
key={idx}
|
key={idx}
|
||||||
question={customInput.label}
|
question={customInput.label}
|
||||||
@@ -144,24 +156,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
setSelectedCustomInput(undefined);
|
setSelectedCustomInput(undefined);
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
}}>
|
}}>
|
||||||
Add an input
|
{t("add_input")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<RequiresConfirmationController
|
||||||
name="requiresConfirmation"
|
seatsEnabled={seatsEnabled}
|
||||||
defaultValue={eventType.requiresConfirmation}
|
metadata={eventType.metadata}
|
||||||
render={({ field: { value, onChange } }) => (
|
requiresConfirmation={requiresConfirmation}
|
||||||
<SettingsToggle
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
title={t("requires_confirmation")}
|
|
||||||
description={t("requires_confirmation_description")}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
disabled={seatsEnabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
@@ -244,7 +249,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
description={t("private_link_description")}
|
description={t("private_link_description", { appName: APP_NAME })}
|
||||||
checked={hashedLinkVisible}
|
checked={hashedLinkVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||||
@@ -297,6 +302,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
if (e) {
|
if (e) {
|
||||||
formMethods.setValue("disableGuests", true);
|
formMethods.setValue("disableGuests", true);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
@@ -342,15 +348,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
title={t("custom_event_name")}
|
title={t("custom_event_name")}
|
||||||
description={t("custom_event_name_description")}
|
description={t("custom_event_name_description")}
|
||||||
type="creation"
|
type="creation">
|
||||||
actionText="Create"
|
|
||||||
// Set event name back to what it was on close
|
|
||||||
actionOnClose={() => formMethods.setValue("eventName", eventType.eventName ?? "")}
|
|
||||||
actionOnClick={() => setShowEventNameTip(false)}>
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
/>
|
/>
|
||||||
@@ -360,6 +362,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
||||||
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
|
||||||
|
{t("create")}
|
||||||
|
</Button>
|
||||||
|
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
@@ -373,29 +381,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
type="creation"
|
type="creation"
|
||||||
Icon={Icon.FiPlus}
|
Icon={Icon.FiPlus}
|
||||||
title={t("add_new_custom_input_field")}
|
title={t("add_new_custom_input_field")}
|
||||||
useOwnActionButtons
|
|
||||||
description={t("this_input_will_shown_booking_this_event")}>
|
description={t("this_input_will_shown_booking_this_event")}>
|
||||||
<CustomInputTypeForm
|
<CustomInputTypeForm
|
||||||
selectedCustomInput={selectedCustomInput}
|
selectedCustomInput={selectedCustomInput}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
const customInput: EventTypeCustomInput = {
|
const customInput: CustomInputParsed = {
|
||||||
id: -1,
|
id: -1,
|
||||||
eventTypeId: -1,
|
eventTypeId: -1,
|
||||||
label: values.label,
|
label: values.label,
|
||||||
placeholder: values.placeholder,
|
placeholder: values.placeholder,
|
||||||
required: values.required,
|
required: values.required,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
|
options: values.options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedCustomInput) {
|
if (selectedCustomInput) {
|
||||||
selectedCustomInput.label = customInput.label;
|
selectedCustomInput.label = customInput.label;
|
||||||
selectedCustomInput.placeholder = customInput.placeholder;
|
selectedCustomInput.placeholder = customInput.placeholder;
|
||||||
selectedCustomInput.required = customInput.required;
|
selectedCustomInput.required = customInput.required;
|
||||||
selectedCustomInput.type = customInput.type;
|
selectedCustomInput.type = customInput.type;
|
||||||
|
selectedCustomInput.options = customInput.options || undefined;
|
||||||
|
// Update by id
|
||||||
|
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
||||||
|
customInputs[inputIndex] = selectedCustomInput;
|
||||||
|
setCustomInputs(customInputs);
|
||||||
|
formMethods.setValue("customInputs", customInputs);
|
||||||
} else {
|
} else {
|
||||||
setCustomInputs(customInputs.concat(customInput));
|
const concatted = customInputs.concat({
|
||||||
formMethods.setValue("customInputs", customInputs.concat(customInput));
|
...customInput,
|
||||||
|
options: customInput.options,
|
||||||
|
});
|
||||||
|
console.log(concatted);
|
||||||
|
setCustomInputs(concatted);
|
||||||
|
formMethods.setValue("customInputs", concatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
setSelectedCustomInputModalOpen(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import React from "react";
|
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
@@ -8,10 +7,7 @@ import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
|||||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
|
||||||
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
|
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import { EmptyScreen } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
|
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ import findDurationType from "@calcom/lib/findDurationType";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { PeriodType } from "@calcom/prisma/client";
|
import { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import { Input, InputField, Label } from "@calcom/ui/components/form";
|
|
||||||
import { Select, SettingsToggle } from "@calcom/ui/v2";
|
|
||||||
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -289,7 +285,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_future_bookings")}
|
title={t("limit_future_bookings")}
|
||||||
description={t("limit_future_bookings_description")}
|
description={t("limit_future_bookings_description")}
|
||||||
checked={value !== "UNLIMITED"}
|
checked={value && value !== "UNLIMITED"}
|
||||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||||
<RadioGroup.Root
|
<RadioGroup.Root
|
||||||
defaultValue={watchPeriodType}
|
defaultValue={watchPeriodType}
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
|
import { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getEventLocationType, EventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
||||||
import { Button } from "@calcom/ui/components";
|
|
||||||
import { Label, TextField } from "@calcom/ui/components/form";
|
|
||||||
import { Select, Skeleton } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
import { slugify } from "@lib/slugify";
|
import { slugify } from "@lib/slugify";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/eventtype/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
|
|
||||||
type OptionTypeBase = {
|
type OptionTypeBase = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -31,7 +29,24 @@ export const EventSetupTab = (
|
|||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team } = props;
|
const { eventType, locationOptions, team } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | 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) => ({
|
||||||
|
value: mins,
|
||||||
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
||||||
|
MultiValue<{
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}>
|
||||||
|
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
|
||||||
|
const [defaultDuration, setDefaultDuration] = useState(
|
||||||
|
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||||
|
);
|
||||||
|
|
||||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
const openLocationModal = (type: EventLocationType["type"]) => {
|
||||||
setSelectedLocation(locationOptions.find((option) => option.value === type));
|
setSelectedLocation(locationOptions.find((option) => option.value === type));
|
||||||
@@ -46,14 +61,23 @@ export const EventSetupTab = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addLocation = (newLocationType: EventLocationType["type"], details = {}) => {
|
const saveLocation = (newLocationType: EventLocationType["type"], details = {}) => {
|
||||||
const existingIdx = formMethods.getValues("locations").findIndex((loc) => newLocationType === loc.type);
|
const locationType = editingLocationType !== "" ? editingLocationType : newLocationType;
|
||||||
|
const existingIdx = formMethods.getValues("locations").findIndex((loc) => locationType === loc.type);
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) {
|
||||||
const copy = formMethods.getValues("locations");
|
const copy = formMethods.getValues("locations");
|
||||||
copy[existingIdx] = {
|
if (editingLocationType !== "") {
|
||||||
...formMethods.getValues("locations")[existingIdx],
|
copy[existingIdx] = {
|
||||||
...details,
|
...details,
|
||||||
};
|
type: newLocationType,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
copy[existingIdx] = {
|
||||||
|
...formMethods.getValues("locations")[existingIdx],
|
||||||
|
...details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
formMethods.setValue("locations", copy);
|
formMethods.setValue("locations", copy);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
@@ -61,6 +85,8 @@ export const EventSetupTab = (
|
|||||||
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
|
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEditingLocationType("");
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +130,7 @@ export const EventSetupTab = (
|
|||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
@@ -118,7 +145,7 @@ export const EventSetupTab = (
|
|||||||
if (eventLocationType.organizerInputType) {
|
if (eventLocationType.organizerInputType) {
|
||||||
openLocationModal(newLocationType);
|
openLocationModal(newLocationType);
|
||||||
} else {
|
} else {
|
||||||
addLocation(newLocationType);
|
saveLocation(newLocationType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -138,10 +165,10 @@ export const EventSetupTab = (
|
|||||||
<div key={index} className="flex flex-grow items-center">
|
<div key={index} className="flex flex-grow items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-6 w-6"
|
className="h-4 w-4"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm ltr:ml-2 rtl:mr-2">
|
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
||||||
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,6 +180,7 @@ export const EventSetupTab = (
|
|||||||
locationFormMethods.unregister("locationLink");
|
locationFormMethods.unregister("locationLink");
|
||||||
locationFormMethods.unregister("locationAddress");
|
locationFormMethods.unregister("locationAddress");
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
locationFormMethods.unregister("locationPhoneNumber");
|
||||||
|
setEditingLocationType(location.type);
|
||||||
openLocationModal(location.type);
|
openLocationModal(location.type);
|
||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
@@ -185,7 +213,7 @@ export const EventSetupTab = (
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("Title")}
|
label={t("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
@@ -209,17 +237,94 @@ export const EventSetupTab = (
|
|||||||
setValueAs: (v) => slugify(v),
|
setValueAs: (v) => slugify(v),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextField
|
{multipleDuration ? (
|
||||||
required
|
<div className="space-y-4">
|
||||||
name="length"
|
<div>
|
||||||
type="number"
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
label={t("duration")}
|
{t("available_durations")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
</Skeleton>
|
||||||
defaultValue={eventType.length ?? 15}
|
<Select
|
||||||
onChange={(e) => {
|
isMulti
|
||||||
formMethods.setValue("length", Number(e.target.value));
|
defaultValue={selectedMultipleDuration}
|
||||||
}}
|
name="metadata.multipleDuration"
|
||||||
/>
|
isSearchable={false}
|
||||||
|
className="h-auto !min-h-[36px] text-sm"
|
||||||
|
options={multipleDurationOptions}
|
||||||
|
value={selectedMultipleDuration}
|
||||||
|
onChange={(options) => {
|
||||||
|
let newOptions = [...options];
|
||||||
|
newOptions = newOptions.sort((a, b) => {
|
||||||
|
return a?.value - b?.value;
|
||||||
|
});
|
||||||
|
const values = newOptions.map((opt) => opt.value);
|
||||||
|
setMultipleDuration(values);
|
||||||
|
setSelectedMultipleDuration(newOptions);
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
|
{t("default_duration")}
|
||||||
|
</Skeleton>
|
||||||
|
<Select
|
||||||
|
value={defaultDuration}
|
||||||
|
isSearchable={false}
|
||||||
|
name="length"
|
||||||
|
className="text-sm"
|
||||||
|
noOptionsMessage={() => t("default_duration_no_options")}
|
||||||
|
options={selectedMultipleDuration}
|
||||||
|
onChange={(option) => {
|
||||||
|
setDefaultDuration(
|
||||||
|
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||||
|
);
|
||||||
|
formMethods.setValue("length", Number(option?.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("allow_booker_to_select_duration")}
|
||||||
|
checked={multipleDuration !== undefined}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (multipleDuration !== undefined) {
|
||||||
|
setMultipleDuration(undefined);
|
||||||
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
|
formMethods.setValue("length", eventType.length);
|
||||||
|
} else {
|
||||||
|
setMultipleDuration([]);
|
||||||
|
formMethods.setValue("metadata.multipleDuration", []);
|
||||||
|
formMethods.setValue("length", 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
@@ -237,12 +342,13 @@ export const EventSetupTab = (
|
|||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={addLocation}
|
saveLocation={saveLocation}
|
||||||
defaultValues={formMethods.getValues("locations")}
|
defaultValues={formMethods.getValues("locations")}
|
||||||
selection={
|
selection={
|
||||||
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
|
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
|
||||||
}
|
}
|
||||||
setSelectedLocation={setSelectedLocation}
|
setSelectedLocation={setSelectedLocation}
|
||||||
|
setEditingLocationType={setEditingLocationType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { Controller, useFormContext } from "react-hook-form";
|
|||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
|
||||||
import { Button, Avatar } from "@calcom/ui/components";
|
|
||||||
import { Label } from "@calcom/ui/components/form";
|
|
||||||
import { Select, CheckedTeamSelect } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IMemberToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
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, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
export const EventTeamWebhooksTab = ({
|
||||||
|
eventType,
|
||||||
|
team,
|
||||||
|
}: Pick<EventTypeSetupInfered, "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={Icon.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={Icon.FiLink}
|
||||||
|
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 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import { TFunction } from "next-i18next";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Loader } from "react-feather";
|
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
@@ -10,20 +9,27 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import {
|
||||||
import { Button, ButtonGroup, Label } from "@calcom/ui/components";
|
Button,
|
||||||
import { HorizontalTabs, showToast, Switch, Tooltip, VerticalTabs } from "@calcom/ui/v2";
|
ButtonGroup,
|
||||||
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
|
ConfirmationDialogContent,
|
||||||
import { Dialog } from "@calcom/ui/v2/core/Dialog";
|
Dialog,
|
||||||
import Divider from "@calcom/ui/v2/core/Divider";
|
Divider,
|
||||||
import Dropdown, {
|
Dropdown,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@calcom/ui/v2/core/Dropdown";
|
HorizontalTabs,
|
||||||
import Shell from "@calcom/ui/v2/core/Shell";
|
Icon,
|
||||||
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
|
Label,
|
||||||
import { Skeleton } from "@calcom/ui/v2/core/skeleton";
|
Shell,
|
||||||
|
showToast,
|
||||||
|
Skeleton,
|
||||||
|
Switch,
|
||||||
|
Tooltip,
|
||||||
|
VerticalDivider,
|
||||||
|
VerticalTabs,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { ClientSuspense } from "@components/ClientSuspense";
|
import { ClientSuspense } from "@components/ClientSuspense";
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
@@ -60,7 +66,7 @@ function getNavigation(props: {
|
|||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
icon: Icon.FiCalendar,
|
icon: Icon.FiCalendar,
|
||||||
info: `Working Hours`, // TODO: Get this from props
|
info: `default_schedule_name`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
@@ -143,13 +149,20 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team)
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "scheduling_type",
|
name: "scheduling_type",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: Icon.FiUsers,
|
icon: Icon.FiUsers,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||||
});
|
});
|
||||||
|
navigation.push({
|
||||||
|
name: "webhooks",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
|
icon: Icon.FiLink,
|
||||||
|
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
||||||
|
|
||||||
@@ -202,7 +215,6 @@ function EventTypeSingleLayout({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
StartIcon={Icon.FiLink}
|
StartIcon={Icon.FiLink}
|
||||||
combined
|
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
@@ -288,7 +300,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<ClientSuspense fallback={<Loader />}>
|
<ClientSuspense fallback={<Icon.FiLoader />}>
|
||||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from "@calcom/features/ee/workflows/components/v2/EventWorkflowsTab";
|
export { default } from "@calcom/features/ee/workflows/components/EventWorkflowsTab";
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { useFormContext } from "react-hook-form";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert, Select, SettingsToggle } from "@calcom/ui";
|
||||||
import { Select, SettingsToggle } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
type RecurringEventControllerProps = {
|
type RecurringEventControllerProps = {
|
||||||
recurringEvent: RecurringEvent | null;
|
recurringEvent: RecurringEvent | null;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import { UnitTypeLongPlural } from "dayjs";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
|
type RequiresConfirmationControllerProps = {
|
||||||
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
|
requiresConfirmation: boolean;
|
||||||
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
|
seatsEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RequiresConfirmationController({
|
||||||
|
metadata,
|
||||||
|
requiresConfirmation,
|
||||||
|
onRequiresConfirmation,
|
||||||
|
seatsEnabled,
|
||||||
|
}: RequiresConfirmationControllerProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
|
||||||
|
metadata?.requiresConfirmationThreshold
|
||||||
|
);
|
||||||
|
const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requiresConfirmation) {
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
}
|
||||||
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="block items-start sm:flex">
|
||||||
|
<div className={!seatsEnabled ? "w-full" : ""}>
|
||||||
|
{seatsEnabled ? (
|
||||||
|
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
name="requiresConfirmation"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("requires_confirmation")}
|
||||||
|
description={t("requires_confirmation_description")}
|
||||||
|
checked={requiresConfirmation}
|
||||||
|
onCheckedChange={(val) => {
|
||||||
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
|
onRequiresConfirmation(val);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={
|
||||||
|
requiresConfirmation
|
||||||
|
? requiresConfirmationSetup === undefined
|
||||||
|
? "always"
|
||||||
|
: "notice"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val === "always") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
setRequiresConfirmationSetup(undefined);
|
||||||
|
} else if (val === "notice") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold",
|
||||||
|
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="always"
|
||||||
|
value="always"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="always" className="!m-0 flex items-center">
|
||||||
|
{t("always_requires_confirmation")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="notice"
|
||||||
|
value="notice"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="notice" className="!m-0 flex items-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
|
defaults="When booked with less than <time></time> notice"
|
||||||
|
components={{
|
||||||
|
time: (
|
||||||
|
<div className="mx-2 flex">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = Number(evt.target?.value);
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
unit:
|
||||||
|
requiresConfirmationSetup?.unit ??
|
||||||
|
defaultRequiresConfirmationSetup.unit,
|
||||||
|
time: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
||||||
|
}}
|
||||||
|
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
||||||
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = evt.target.value as UnitTypeLongPlural;
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
time:
|
||||||
|
requiresConfirmationSetup?.time ??
|
||||||
|
defaultRequiresConfirmationSetup.time,
|
||||||
|
unit: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
|
||||||
|
}}
|
||||||
|
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||||
|
defaultValue={
|
||||||
|
metadata?.requiresConfirmationThreshold?.unit ||
|
||||||
|
defaultRequiresConfirmationSetup.unit
|
||||||
|
}>
|
||||||
|
<option value="minutes">{t("minute_timeUnit")}</option>
|
||||||
|
<option value="hours">{t("hour_timeUnit")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon, SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/components";
|
import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
interface ICalendarItem {
|
interface ICalendarItem {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Switch } from "@calcom/ui/v2";
|
import { showToast, Switch } from "@calcom/ui";
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { ArrowRightIcon } from "@heroicons/react/solid";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { List } from "@calcom/ui/List";
|
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
|
||||||
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui/v2";
|
|
||||||
|
|
||||||
import { CalendarItem } from "../components/CalendarItem";
|
import { CalendarItem } from "../components/CalendarItem";
|
||||||
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button, Form } from "@calcom/ui";
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
|
|||||||
@@ -4,24 +4,20 @@ import { FormEvent, useRef, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui/components";
|
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
||||||
import { TextArea } from "@calcom/ui/components/form";
|
import { Avatar } from "@calcom/ui";
|
||||||
import { showToast } from "@calcom/ui/v2";
|
|
||||||
import ImageUploader from "@calcom/ui/v2/core/ImageUploader";
|
|
||||||
|
|
||||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserProfile {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
|
interface IUserProfileProps {
|
||||||
|
user: IOnboardingPageProps["user"];
|
||||||
|
}
|
||||||
|
|
||||||
const UserProfile = (props: IUserProfile) => {
|
const UserProfile = (props: IUserProfileProps) => {
|
||||||
const { user } = props;
|
const { user } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||||
@@ -103,7 +99,14 @@ const UserProfile = (props: IUserProfile) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||||
{user && <AvatarSSR user={user} alt="Profile picture" className="h-16 w-16" />}
|
{user && (
|
||||||
|
<Avatar
|
||||||
|
alt={user.username || "user avatar"}
|
||||||
|
gravatarFallbackMd5={user.emailMd5}
|
||||||
|
size="lg"
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
|
||||||
|
|
||||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserSettingsProps {
|
interface IUserSettingsProps {
|
||||||
user: User;
|
user: IOnboardingPageProps["user"];
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,11 +24,9 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
control,
|
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: user?.name || "",
|
name: user?.name || "",
|
||||||
username: user?.username || "",
|
|
||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
@@ -42,39 +40,19 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
});
|
});
|
||||||
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
|
|
||||||
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (paymentRequired) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
timeZone: selectedTimeZone,
|
timeZone: selectedTimeZone,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentUsername, setCurrentUsername] = useState(user.username || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Username textfield */}
|
{/* Username textfield */}
|
||||||
<Controller
|
<UsernameAvailabilityField user={user} />
|
||||||
control={control}
|
|
||||||
name="username"
|
|
||||||
render={({ field: { value, ref, onChange } }) => (
|
|
||||||
<UsernameAvailability
|
|
||||||
readonly={true}
|
|
||||||
currentUsername={currentUsername}
|
|
||||||
setCurrentUsername={setCurrentUsername}
|
|
||||||
inputUsernameValue={value}
|
|
||||||
usernameRef={ref}
|
|
||||||
setInputUsernameValue={onChange}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
|
||||||
import { List } from "@calcom/ui/List";
|
|
||||||
import { ShellSubHeading } from "@calcom/ui/Shell";
|
|
||||||
import Switch from "@calcom/ui/Switch";
|
|
||||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
import AdditionalCalendarSelector from "@components/AdditionalCalendarSelector";
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
|
||||||
|
|
||||||
import DisconnectIntegration from "./DisconnectIntegration";
|
|
||||||
import IntegrationListItem from "./IntegrationListItem";
|
|
||||||
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onChanged: () => unknown | Promise<unknown>;
|
|
||||||
fromOnboarding?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function CalendarSwitch(props: {
|
|
||||||
type: string;
|
|
||||||
externalId: string;
|
|
||||||
title: string;
|
|
||||||
defaultSelected: boolean;
|
|
||||||
}) {
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const mutation = useMutation<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
isOn: boolean;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
async ({ isOn }) => {
|
|
||||||
const body = {
|
|
||||||
integration: props.type,
|
|
||||||
externalId: props.externalId,
|
|
||||||
};
|
|
||||||
if (isOn) {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.viewer.integrations.invalidate();
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="py-1">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.defaultSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CalendarList(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => (
|
|
||||||
<List>
|
|
||||||
{data.items.map((item) => (
|
|
||||||
<IntegrationListItem
|
|
||||||
name={item.name}
|
|
||||||
slug={item.slug}
|
|
||||||
key={item.title}
|
|
||||||
title={item.title}
|
|
||||||
logo={item.logo}
|
|
||||||
description={item.description}
|
|
||||||
actions={
|
|
||||||
<InstallAppButton
|
|
||||||
type={item.type}
|
|
||||||
render={(buttonProps) => (
|
|
||||||
<Button color="secondary" {...buttonProps}>
|
|
||||||
{t("connect")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onChanged={() => props.onChanged()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectedCalendarsList(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
|
||||||
suspense: true,
|
|
||||||
trpc: {},
|
|
||||||
});
|
|
||||||
const { fromOnboarding } = props;
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
empty={() => null}
|
|
||||||
success={({ data }) => {
|
|
||||||
if (!data.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<List>
|
|
||||||
{data.connectedCalendars.map((item) => (
|
|
||||||
<Fragment key={item.credentialId}>
|
|
||||||
{item.calendars ? (
|
|
||||||
<IntegrationListItem
|
|
||||||
slug={item.integration.slug}
|
|
||||||
title={item.integration.title}
|
|
||||||
logo={item.integration.logo}
|
|
||||||
description={item.primary?.externalId || "No external Id"}
|
|
||||||
actions={
|
|
||||||
<DisconnectIntegration
|
|
||||||
id={item.credentialId}
|
|
||||||
externalId={item.primary?.externalId}
|
|
||||||
render={(btnProps) => (
|
|
||||||
<Button {...btnProps} color="secondary" data-testid="integration-connection-button">
|
|
||||||
{t("disconnect")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onOpenChange={props.onChanged}
|
|
||||||
/>
|
|
||||||
}>
|
|
||||||
{!fromOnboarding && (
|
|
||||||
<>
|
|
||||||
<p className="px-4 pt-4 text-sm text-neutral-500">{t("toggle_calendars_conflict")}</p>
|
|
||||||
<ul className="space-y-2 p-4">
|
|
||||||
{item.calendars.map((cal) => (
|
|
||||||
<CalendarSwitch
|
|
||||||
key={cal.externalId}
|
|
||||||
externalId={cal.externalId}
|
|
||||||
title={cal.name || "Nameless calendar"}
|
|
||||||
type={item.integration.type}
|
|
||||||
defaultSelected={cal.isSelected}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</IntegrationListItem>
|
|
||||||
) : (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
title={t("something_went_wrong")}
|
|
||||||
message={t("calendar_error")}
|
|
||||||
actions={
|
|
||||||
<DisconnectIntegration
|
|
||||||
id={item.credentialId}
|
|
||||||
render={(btnProps) => (
|
|
||||||
<Button {...btnProps} color="secondary" data-testid="integration-connection-button">
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onOpenChange={() => props.onChanged()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CalendarListContainer(props: {
|
|
||||||
heading?: boolean;
|
|
||||||
items?: RouterOutputs["viewer"]["integrations"]["items"];
|
|
||||||
fromOnboarding?: boolean;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { heading = true, fromOnboarding } = props;
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const onChanged = () =>
|
|
||||||
Promise.allSettled([
|
|
||||||
utils.viewer.integrations.invalidate(
|
|
||||||
{ variant: "calendar", onlyInstalled: true },
|
|
||||||
{
|
|
||||||
exact: true,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
utils.viewer.connectedCalendars.invalidate(),
|
|
||||||
]);
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
|
||||||
const mutation = trpc.viewer.setDestinationCalendar.useMutation();
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
customLoader={<SkeletonLoader className="mt-10" />}
|
|
||||||
success={({ data }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{(!!data.connectedCalendars.length || !!installedCalendars.data?.items.length) && (
|
|
||||||
<>
|
|
||||||
{heading && (
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-10 mb-0"
|
|
||||||
title={
|
|
||||||
<SubHeadingTitleWithConnections
|
|
||||||
title="Calendars"
|
|
||||||
numConnections={data.connectedCalendars.length}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
subtitle={t("configure_how_your_event_types_interact")}
|
|
||||||
actions={
|
|
||||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
|
||||||
<div className="sm:min-w-80 block max-w-full">
|
|
||||||
<DestinationCalendarSelector
|
|
||||||
onChange={mutation.mutate}
|
|
||||||
isLoading={mutation.isLoading}
|
|
||||||
value={data.destinationCalendar?.externalId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!data.connectedCalendars.length && (
|
|
||||||
<div className="sm:min-w-80 inline max-w-full">
|
|
||||||
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ConnectedCalendarsList onChanged={onChanged} fromOnboarding={fromOnboarding} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{fromOnboarding && (
|
|
||||||
<>
|
|
||||||
{!!query.data?.connectedCalendars.length && (
|
|
||||||
<ShellSubHeading
|
|
||||||
className="mt-4"
|
|
||||||
title={<SubHeadingTitleWithConnections title={t("connect_additional_calendar")} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CalendarList onChanged={onChanged} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
|
||||||
import { Dialog } from "@calcom/ui/Dialog";
|
|
||||||
import { ButtonBaseProps } from "@calcom/ui/components/button";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
export default function DisconnectIntegration(props: {
|
|
||||||
/** Integration credential id */
|
|
||||||
id: number;
|
|
||||||
externalId?: string;
|
|
||||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
|
||||||
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
|
||||||
}) {
|
|
||||||
const { id, externalId = "" } = props;
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const mutation = trpc.viewer.deleteCredential.useMutation({
|
|
||||||
onSettled: async () => {
|
|
||||||
await props.onOpenChange(modalOpen);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
showToast("Integration deleted successfully", "success");
|
|
||||||
setModalOpen(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title={t("remove_app")}
|
|
||||||
confirmBtnText={t("yes_remove_app")}
|
|
||||||
cancelBtnText="Cancel"
|
|
||||||
onConfirm={() => {
|
|
||||||
mutation.mutate({ id, externalId });
|
|
||||||
}}>
|
|
||||||
{t("are_you_sure_you_want_to_remove_this_app")}
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
{props.render({
|
|
||||||
onClick() {
|
|
||||||
setModalOpen(true);
|
|
||||||
},
|
|
||||||
disabled: modalOpen,
|
|
||||||
loading: mutation.isLoading,
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/List";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
|
|
||||||
function IntegrationListItem(props: {
|
|
||||||
imageSrc?: string;
|
|
||||||
slug: string;
|
|
||||||
name?: string;
|
|
||||||
title?: string;
|
|
||||||
description: string;
|
|
||||||
actions?: ReactNode;
|
|
||||||
children?: ReactNode;
|
|
||||||
logo: string;
|
|
||||||
}): JSX.Element {
|
|
||||||
const title = props.name || props.title;
|
|
||||||
return (
|
|
||||||
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
|
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
props.logo && <img className="h-10 w-10" src={props.logo} alt={title} />
|
|
||||||
}
|
|
||||||
<div className="flex-grow truncate pl-2">
|
|
||||||
<ListItemTitle component="h3">
|
|
||||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
|
||||||
</ListItemTitle>
|
|
||||||
<ListItemText component="p">{props.description}</ListItemText>
|
|
||||||
</div>
|
|
||||||
<div>{props.actions}</div>
|
|
||||||
</div>
|
|
||||||
{props.children && <div className="w-full border-t border-gray-200">{props.children}</div>}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntegrationListItem;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
import { Badge } from "@calcom/ui";
|
||||||
|
|
||||||
function pluralize(opts: { num: number; plural: string; singular: string }) {
|
function pluralize(opts: { num: number; plural: string; singular: string }) {
|
||||||
if (opts.num === 0) {
|
if (opts.num === 0) {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
|
||||||
label: string;
|
|
||||||
value: EventTypeCustomInputType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
|
||||||
onCancel: () => void;
|
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const inputOptions: OptionTypeBase[] = [
|
|
||||||
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
|
|
||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
|
||||||
];
|
|
||||||
const { selectedCustomInput } = props;
|
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
|
||||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(props.onSubmit)}>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("input_type")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="type"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
id="type"
|
|
||||||
defaultValue={selectedInputOption}
|
|
||||||
options={inputOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
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}
|
|
||||||
name={field.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("label")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="label"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.label}
|
|
||||||
{...register("label", { required: true })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
|
||||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("placeholder")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="placeholder"
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
|
||||||
{...register("placeholder")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="required"
|
|
||||||
type="checkbox"
|
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultChecked={selectedCustomInput?.required ?? true}
|
|
||||||
{...register("required")}
|
|
||||||
/>
|
|
||||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("is_required")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="eventTypeId"
|
|
||||||
value={selectedCustomInput?.eventTypeId || -1}
|
|
||||||
{...register("eventTypeId", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id"
|
|
||||||
value={selectedCustomInput?.id || -1}
|
|
||||||
{...register("id", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="mt-5 flex space-x-2 sm:mt-4">
|
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("save")}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Attendee, Booking, User } from "@prisma/client";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { JsonLd } from "react-schemaorg";
|
||||||
|
import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
|
||||||
|
|
||||||
|
type EventSchemaUser = Pick<User, "name" | "email">;
|
||||||
|
type EventSchemaAttendee = Pick<Attendee, "name" | "email">;
|
||||||
|
|
||||||
|
interface EventReservationSchemaInterface {
|
||||||
|
reservationId: Booking["uid"];
|
||||||
|
eventName: Booking["title"];
|
||||||
|
startTime: Booking["startTime"];
|
||||||
|
endTime: Booking["endTime"];
|
||||||
|
organizer: EventSchemaUser | null;
|
||||||
|
attendees: EventSchemaAttendee[];
|
||||||
|
location: Booking["location"];
|
||||||
|
description: Booking["description"];
|
||||||
|
status: Booking["status"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
|
||||||
|
reservationId,
|
||||||
|
eventName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
organizer,
|
||||||
|
attendees,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
const reservationStatus = useMemo<ReservationStatusType>(() => {
|
||||||
|
switch (status) {
|
||||||
|
case "ACCEPTED":
|
||||||
|
return "ReservationConfirmed";
|
||||||
|
case "REJECTED":
|
||||||
|
case "CANCELLED":
|
||||||
|
return "ReservationCancelled";
|
||||||
|
case "PENDING":
|
||||||
|
return "ReservationPending";
|
||||||
|
default:
|
||||||
|
return "ReservationHold";
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JsonLd<EventReservation>
|
||||||
|
item={{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "EventReservation",
|
||||||
|
reservationId,
|
||||||
|
reservationStatus,
|
||||||
|
reservationFor: {
|
||||||
|
"@type": "Event",
|
||||||
|
name: eventName,
|
||||||
|
startDate: startTime.toString(),
|
||||||
|
endDate: endTime.toString(),
|
||||||
|
organizer: organizer
|
||||||
|
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
|
||||||
|
: undefined,
|
||||||
|
attendee: attendees?.map(
|
||||||
|
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
|
||||||
|
),
|
||||||
|
location: location || undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventReservationSchema;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { SyntheticEvent, useState } from "react";
|
import { SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
const ChangePasswordSection = () => {
|
const ChangePasswordSection = () => {
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
const [oldPassword, setOldPassword] = useState("");
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import { PasswordField } from "@calcom/ui/components/form";
|
|
||||||
import { Form, Label } from "@calcom/ui/form/fields";
|
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
import { Badge, Button, showToast } from "@calcom/ui";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import { Form } from "@calcom/ui/components/form";
|
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import { Badge, Button } from "@calcom/ui";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from "react";
|
import { ShieldCheckIcon } from "@calcom/ui";
|
||||||
|
|
||||||
import { ShieldCheckIcon } from "@calcom/ui/Icon";
|
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MeetingImageProps,
|
MeetingImageProps,
|
||||||
} from "@calcom/lib/OgImages";
|
} from "@calcom/lib/OgImages";
|
||||||
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
|
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
|
|
||||||
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
||||||
@@ -79,7 +80,7 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
|||||||
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
|
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
|
||||||
const truncatedDescription = truncateOnWord(description, 158);
|
const truncatedDescription = truncateOnWord(description, 158);
|
||||||
|
|
||||||
const pageTitle = title + " | Cal.com";
|
const pageTitle = title + " | " + APP_NAME;
|
||||||
let seoObject = buildSeoMeta({
|
let seoObject = buildSeoMeta({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
image,
|
image,
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Badge, Icon, showToast, Switch } from "@calcom/ui";
|
||||||
import { Badge } from "@calcom/ui/components/badge";
|
|
||||||
import Switch from "@calcom/ui/v2/core/Switch";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
export function CalendarSwitch(props: {
|
||||||
type: string;
|
type: string;
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||||
import { Form, Label } from "@calcom/ui/components/form";
|
|
||||||
import { PasswordField } from "@calcom/ui/components/form";
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
@@ -75,11 +72,7 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
|
||||||
title={t("disable_2fa")}
|
|
||||||
description={t("disable_2fa_recommendation")}
|
|
||||||
type="creation"
|
|
||||||
useOwnActionButtons>
|
|
||||||
<Form form={form} handleSubmit={handleDisable}>
|
<Form form={form} handleSubmit={handleDisable}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
|
||||||
import { Form, TextField } from "@calcom/ui/components/form";
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
@@ -132,11 +130,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
|
||||||
title={t("enable_2fa")}
|
|
||||||
description={setupDescriptions[step]}
|
|
||||||
type="creation"
|
|
||||||
useOwnActionButtons>
|
|
||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<form onSubmit={handleSetup}>
|
<form onSubmit={handleSetup}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { objectKeys } from "@calcom/lib/objectKeys";
|
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { TextField } from "@calcom/ui/form/fields";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
import ImageUploader from "@components/ImageUploader";
|
|
||||||
import SettingInputContainer from "@components/ui/SettingInputContainer";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
team: TeamWithMembers | null | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TeamSettings(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const [hasErrors, setHasErrors] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
|
||||||
|
|
||||||
const team = props.team;
|
|
||||||
const hasLogo = !!team?.logo;
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const mutation = trpc.viewer.teams.update.useMutation({
|
|
||||||
onError: (err) => {
|
|
||||||
setHasErrors(true);
|
|
||||||
setErrorMessage(err.message);
|
|
||||||
},
|
|
||||||
async onSuccess() {
|
|
||||||
await utils.viewer.teams.get.invalidate();
|
|
||||||
showToast(t("your_team_updated_successfully"), "success");
|
|
||||||
setHasErrors(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const teamUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>;
|
|
||||||
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
function updateTeamData() {
|
|
||||||
if (!team) return;
|
|
||||||
const variables = {
|
|
||||||
name: nameRef.current?.value,
|
|
||||||
slug: teamUrlRef.current?.value,
|
|
||||||
bio: descriptionRef.current?.value,
|
|
||||||
hideBranding: hideBrandingRef.current?.checked,
|
|
||||||
};
|
|
||||||
// remove unchanged variables
|
|
||||||
objectKeys(variables).forEach((key) => {
|
|
||||||
if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key];
|
|
||||||
});
|
|
||||||
mutation.mutate({ id: team.id, ...variables });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLogo(newLogo: string) {
|
|
||||||
if (!team) return;
|
|
||||||
logoRef.current.value = newLogo;
|
|
||||||
mutation.mutate({ id: team.id, logo: newLogo });
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLogo = () => updateLogo("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
|
||||||
<div className="">
|
|
||||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
|
||||||
<form
|
|
||||||
className="divide-y divide-gray-200 lg:col-span-9"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
updateTeamData();
|
|
||||||
}}>
|
|
||||||
<div className="py-6">
|
|
||||||
<div className="flex flex-col lg:flex-row">
|
|
||||||
<div className="flex-grow space-y-6">
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiLink}
|
|
||||||
label="Team URL"
|
|
||||||
htmlFor="team-url"
|
|
||||||
Input={
|
|
||||||
<TextField
|
|
||||||
name="" // typescript requires name but we don't want component to render name label
|
|
||||||
id="team-url"
|
|
||||||
addOnLeading={
|
|
||||||
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
ref={teamUrlRef}
|
|
||||||
defaultValue={team?.slug as string}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiHash}
|
|
||||||
label="Team Name"
|
|
||||||
htmlFor="name"
|
|
||||||
Input={
|
|
||||||
<input
|
|
||||||
ref={nameRef}
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder={t("your_team_name")}
|
|
||||||
required
|
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
|
|
||||||
defaultValue={team?.name as string}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiInfo}
|
|
||||||
label={t("about")}
|
|
||||||
htmlFor="about"
|
|
||||||
Input={
|
|
||||||
<>
|
|
||||||
<textarea
|
|
||||||
ref={descriptionRef}
|
|
||||||
id="about"
|
|
||||||
name="about"
|
|
||||||
rows={3}
|
|
||||||
defaultValue={team?.bio as string}
|
|
||||||
className="mt-1 block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
/>
|
|
||||||
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiImage}
|
|
||||||
label="Logo"
|
|
||||||
htmlFor="avatar"
|
|
||||||
Input={
|
|
||||||
<>
|
|
||||||
<div className="mt-1 flex">
|
|
||||||
<input
|
|
||||||
ref={logoRef}
|
|
||||||
type="hidden"
|
|
||||||
name="avatar"
|
|
||||||
id="avatar"
|
|
||||||
placeholder="URL"
|
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
|
|
||||||
defaultValue={team?.logo ?? undefined}
|
|
||||||
/>
|
|
||||||
<ImageUploader
|
|
||||||
target="logo"
|
|
||||||
id="logo-upload"
|
|
||||||
buttonMsg={hasLogo ? t("edit_logo") : t("upload_a_logo")}
|
|
||||||
handleAvatarChange={updateLogo}
|
|
||||||
imageSrc={team?.logo ?? undefined}
|
|
||||||
/>
|
|
||||||
{hasLogo && (
|
|
||||||
<Button
|
|
||||||
onClick={removeLogo}
|
|
||||||
color="secondary"
|
|
||||||
type="button"
|
|
||||||
className="ml-1 py-1 text-xs">
|
|
||||||
{t("remove_logo")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr className="mt-6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex items-start">
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="hide-branding"
|
|
||||||
name="hide-branding"
|
|
||||||
type="checkbox"
|
|
||||||
ref={hideBrandingRef}
|
|
||||||
defaultChecked={team?.hideBranding}
|
|
||||||
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
|
||||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
|
||||||
{t("disable_cal_branding")}
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end py-4">
|
|
||||||
<Button type="submit" color="primary">
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { MembershipRole } from "@prisma/client";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
|
||||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import showToast from "@calcom/ui/v2/core/notifications";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
|
||||||
import LinkIconButton from "@components/ui/LinkIconButton";
|
|
||||||
|
|
||||||
export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers; role: MembershipRole }) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
|
|
||||||
|
|
||||||
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
|
|
||||||
async onSuccess() {
|
|
||||||
await utils.viewer.teams.get.invalidate();
|
|
||||||
router.push(`/settings/teams`);
|
|
||||||
showToast(t("your_team_updated_successfully"), "success");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.viewer.teams.list.invalidate();
|
|
||||||
router.push(`/settings/teams`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function deleteTeam() {
|
|
||||||
if (props.team?.id) deleteTeamMutation.mutate({ teamId: props.team.id });
|
|
||||||
}
|
|
||||||
function leaveTeam() {
|
|
||||||
if (props.team?.id)
|
|
||||||
acceptOrLeaveMutation.mutate({
|
|
||||||
teamId: props.team.id,
|
|
||||||
accept: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 px-2">
|
|
||||||
{(props.role === MembershipRole.OWNER || props.role === MembershipRole.ADMIN) && (
|
|
||||||
<CreateEventTypeButton
|
|
||||||
isIndividualTeam
|
|
||||||
canAddEvents={true}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
teamId: props.team?.id,
|
|
||||||
name: props.team?.name,
|
|
||||||
slug: props.team?.slug,
|
|
||||||
image: props.team?.logo,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Link href={permalink} passHref={true}>
|
|
||||||
<a target="_blank">
|
|
||||||
<LinkIconButton Icon={Icon.FiExternalLink}>{t("preview")}</LinkIconButton>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<LinkIconButton
|
|
||||||
Icon={Icon.FiLink}
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(permalink);
|
|
||||||
showToast("Copied to clipboard", "success");
|
|
||||||
}}>
|
|
||||||
{t("copy_link_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
{props.role === "OWNER" ? (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<LinkIconButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
Icon={Icon.FiTrash}>
|
|
||||||
{t("disband_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title={t("disband_team")}
|
|
||||||
confirmBtnText={t("confirm_disband_team")}
|
|
||||||
onConfirm={deleteTeam}>
|
|
||||||
{t("disband_team_confirmation_message")}
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
) : (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<LinkIconButton
|
|
||||||
Icon={Icon.FiLogOut}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}>
|
|
||||||
{t("leave_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title={t("leave_team")}
|
|
||||||
confirmBtnText={t("confirm_leave_team")}
|
|
||||||
onConfirm={leaveTeam}>
|
|
||||||
{t("leave_team_confirmation_message")}
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{props.team?.id && props.role !== MembershipRole.MEMBER && (
|
|
||||||
<Link href={`/settings/teams/${props.team.id}/availability`}>
|
|
||||||
<div className="mt-5 hidden space-y-1 sm:block">
|
|
||||||
<LinkIconButton Icon={Icon.FiClock}>View Availability</LinkIconButton>
|
|
||||||
<p className="mt-2 text-sm text-gray-500">See your team members availability at a glance.</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { TeamPageProps } from "pages/team/[slug]";
|
import { TeamPageProps } from "pages/team/[slug]";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { Avatar } from "@calcom/ui/components";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { LOGO } from "@calcom/lib/constants";
|
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
@@ -21,7 +21,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
<HeadSeo title={props.title} description={props.description} />
|
<HeadSeo title={props.title} description={props.description} />
|
||||||
{props.showLogo && (
|
{props.showLogo && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" />
|
<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")}>
|
<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-neutral-900">{props.heading}</h2>}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -12,6 +13,7 @@ export type AvatarGroupProps = {
|
|||||||
image: string;
|
image: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
href?: string;
|
||||||
}[];
|
}[];
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@@ -21,15 +23,25 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
|||||||
<ul className={classNames(props.className)}>
|
<ul className={classNames(props.className)}>
|
||||||
{props.items.slice(0, props.truncateAfter).map((item, idx) => {
|
{props.items.slice(0, props.truncateAfter).map((item, idx) => {
|
||||||
if (item.image != null) {
|
if (item.image != null) {
|
||||||
|
const avatar = (
|
||||||
|
<AvatarSSR
|
||||||
|
className={props.border}
|
||||||
|
imageSrc={item.image}
|
||||||
|
title={item.title}
|
||||||
|
alt={item.alt || ""}
|
||||||
|
size={props.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={idx} className="-mr-2 inline-block">
|
<li key={idx} className="-mr-2 inline-block">
|
||||||
<AvatarSSR
|
{item.href ? (
|
||||||
className={props.border}
|
<Link href={item.href}>
|
||||||
imageSrc={item.image}
|
<a>{avatar}</a>
|
||||||
title={item.title}
|
</Link>
|
||||||
alt={item.alt || ""}
|
) : (
|
||||||
size={props.size}
|
avatar
|
||||||
/>
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type AvatarProps = (
|
|||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
href?: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ export function AvatarSSR(props: AvatarProps) {
|
|||||||
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
|
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
|
||||||
return title ? (
|
return title ? (
|
||||||
<Tooltip.Tooltip delayDuration={300}>
|
<Tooltip.Tooltip delayDuration={300}>
|
||||||
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
|
<Tooltip.TooltipTrigger asChild>{avatar}</Tooltip.TooltipTrigger>
|
||||||
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
|
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
|
||||||
<Tooltip.Arrow />
|
<Tooltip.Arrow />
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import classNames from "classnames";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ControllerRenderProps } from "react-hook-form";
|
import { ControllerRenderProps } from "react-hook-form";
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
const EditableHeading = function EditableHeading({
|
const EditableHeading = function EditableHeading({
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon, Tooltip } from "@calcom/ui";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
|
||||||
|
|
||||||
export default function InfoBadge({ content }: { content: string }) {
|
export default function InfoBadge({ content }: { content: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
import { Dialog, DialogContent } from "@calcom/ui";
|
||||||
|
|
||||||
export default function ModalContainer(
|
export default function ModalContainer(
|
||||||
props: PropsWithChildren<{
|
props: PropsWithChildren<{
|
||||||
|
|||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
import { cleanup, screen, render } from "@testing-library/react";
|
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
import NoCalendarConnectedAlert from "..";
|
|
||||||
|
|
||||||
// TODO: useQuery mock is not working
|
|
||||||
describe.skip("Testing NoCalendarConnectedAlert", () => {
|
|
||||||
describe("Render test", () => {
|
|
||||||
it("should render without crashing", () => {
|
|
||||||
// Disabled as its asking for full trpc useQuery response
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
jest.spyOn(trpc, "useQuery").mockReturnValue({
|
|
||||||
isSuccess: true,
|
|
||||||
isFetched: true,
|
|
||||||
data: { connectedCalendars: [], destinationCalendar: undefined },
|
|
||||||
});
|
|
||||||
render(<NoCalendarConnectedAlert />);
|
|
||||||
|
|
||||||
const testProp = screen.getByText("missing_connected_calendar");
|
|
||||||
expect(testProp).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not render", () => {
|
|
||||||
// Disabled as its asking for full trpc useQuery response
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
jest.spyOn(trpc, "useQuery").mockReturnValue({
|
|
||||||
isSuccess: true,
|
|
||||||
isFetched: true,
|
|
||||||
data: { connectedCalendars: [1, 2], destinationCalendar: { a: "test" } },
|
|
||||||
});
|
|
||||||
render(<NoCalendarConnectedAlert />);
|
|
||||||
|
|
||||||
expect(screen.queryByText("missing_connected_calendar")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(cleanup);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
// @TODO: alert v2
|
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
|
||||||
|
|
||||||
import { LinkText } from "../LinkText";
|
|
||||||
|
|
||||||
const NoCalendarConnectedAlert = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
// We are not gonna show this alert till we fetch data from DB
|
|
||||||
let defaultCalendarConnected = true;
|
|
||||||
if (query.isSuccess && query.isFetched && query.data) {
|
|
||||||
defaultCalendarConnected =
|
|
||||||
query.data.connectedCalendars.length > 0 && query.data.destinationCalendar !== undefined;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!defaultCalendarConnected && (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
className="mb-4"
|
|
||||||
title={<>{t("missing_connected_calendar") as string}</>}
|
|
||||||
message={
|
|
||||||
<Trans i18nKey="connect_your_calendar_and_link">
|
|
||||||
You can connect your calendar from
|
|
||||||
<LinkText
|
|
||||||
data-testid="no-calendar-connected-alert"
|
|
||||||
href="/apps/categories/calendar"
|
|
||||||
classNameChildren="underline">
|
|
||||||
here
|
|
||||||
</LinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default NoCalendarConnectedAlert;
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
|
||||||
import Dropdown, {
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
} from "@calcom/ui/Dropdown";
|
DropdownMenuTrigger,
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
Icon,
|
||||||
import { Button } from "@calcom/ui/components/button";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { RefCallback, useMemo, useEffect, useState } from "react";
|
import { RefCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -11,15 +11,20 @@ import { User } from "@calcom/prisma/client";
|
|||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
|
import {
|
||||||
import { Icon, StarIconSolid } from "@calcom/ui/Icon";
|
Button,
|
||||||
import { Button } from "@calcom/ui/components/button";
|
Dialog,
|
||||||
import { Input, Label } from "@calcom/ui/components/form";
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
StarIconSolid,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
NORMAL = "NORMAL",
|
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
DOWNGRADE = "DOWNGRADE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
@@ -30,43 +35,21 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<
|
user: Pick<User, "username" | "metadata">;
|
||||||
User,
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "bio"
|
|
||||||
| "avatar"
|
|
||||||
| "timeZone"
|
|
||||||
| "weekStart"
|
|
||||||
| "hideBranding"
|
|
||||||
| "theme"
|
|
||||||
| "plan"
|
|
||||||
| "brandColor"
|
|
||||||
| "darkBrandColor"
|
|
||||||
| "timeFormat"
|
|
||||||
| "metadata"
|
|
||||||
>;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obtainNewUsernameChangeCondition = ({
|
const obtainNewUsernameChangeCondition = ({
|
||||||
userIsPremium,
|
userIsPremium,
|
||||||
isNewUsernamePremium,
|
isNewUsernamePremium,
|
||||||
stripeCustomer,
|
|
||||||
}: {
|
}: {
|
||||||
userIsPremium: boolean;
|
userIsPremium: boolean;
|
||||||
isNewUsernamePremium: boolean;
|
isNewUsernamePremium: boolean;
|
||||||
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) {
|
if (!userIsPremium && isNewUsernamePremium) {
|
||||||
return UsernameChangeStatusEnum.UPGRADE;
|
return UsernameChangeStatusEnum.UPGRADE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") {
|
|
||||||
return UsernameChangeStatusEnum.DOWNGRADE;
|
|
||||||
}
|
|
||||||
return UsernameChangeStatusEnum.NORMAL;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||||
@@ -99,7 +82,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
setIsInputUsernamePremium(data.premium);
|
setIsInputUsernamePremium(data.premium);
|
||||||
setUsernameIsAvailable(data.available);
|
setUsernameIsAvailable(data.available);
|
||||||
}, 150),
|
}, 150),
|
||||||
[]
|
[currentUsername]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -130,7 +113,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
||||||
const paymentRequired = !currentUsername && stripeCustomer?.isPremium && !stripeCustomer?.paidForPremium;
|
const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
|
||||||
|
|
||||||
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
||||||
userIsPremium: isCurrentUsernamePremium,
|
userIsPremium: isCurrentUsernamePremium,
|
||||||
@@ -187,7 +170,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveUsername = () => {
|
const saveUsername = () => {
|
||||||
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) {
|
if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
|
||||||
updateUsername.mutate({
|
updateUsername.mutate({
|
||||||
username: inputUsernameValue,
|
username: inputUsernameValue,
|
||||||
});
|
});
|
||||||
@@ -272,14 +255,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{paymentMsg}
|
{paymentMsg}
|
||||||
{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">Username is already taken</p>}
|
||||||
|
|
||||||
{usernameIsAvailable && (
|
|
||||||
<p className={classNames("mt-1 text-xs text-gray-900")}>
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
|
|
||||||
<>{t("premium_to_standard_username_description")}</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -288,13 +263,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-4 w-full px-4 pt-1">
|
<div className="mb-4 w-full px-4 pt-1">
|
||||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||||
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<p className="-mt-4 mb-4 text-sm text-gray-800">
|
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
|
|
||||||
t("change_username_standard_to_premium")}
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
|
|
||||||
t("change_username_premium_to_standard")}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
||||||
@@ -316,8 +286,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
||||||
{/* redirect to checkout */}
|
{/* redirect to checkout */}
|
||||||
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE ||
|
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
@@ -329,7 +298,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Normal save */}
|
{/* Normal save */}
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
@@ -340,10 +309,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<DialogClose asChild>
|
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
||||||
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
{t("cancel")}
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { Button, Input, Label } from "@calcom/ui/components";
|
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
currentUsername: string | undefined;
|
currentUsername: string | undefined;
|
||||||
@@ -126,6 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<Input
|
<Input
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
|
value={inputUsernameValue}
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
@@ -135,7 +134,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||||
: ""
|
: ""
|
||||||
)}
|
)}
|
||||||
defaultValue={currentUsername}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setInputUsernameValue(event.target.value);
|
setInputUsernameValue(event.target.value);
|
||||||
@@ -196,10 +194,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DialogClose asChild>
|
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
||||||
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
{t("cancel")}
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
|
import { User } from "@calcom/prisma/client";
|
||||||
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
import { PremiumTextfield } from "./PremiumTextfield";
|
import { PremiumTextfield } from "./PremiumTextfield";
|
||||||
import { UsernameTextfield } from "./UsernameTextfield";
|
import { UsernameTextfield } from "./UsernameTextfield";
|
||||||
|
|
||||||
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
||||||
|
|
||||||
|
interface UsernameAvailabilityFieldProps {
|
||||||
|
onSuccessMutation?: () => void;
|
||||||
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
|
user: Pick<User, "username" | "metadata">;
|
||||||
|
}
|
||||||
|
export const UsernameAvailabilityField = ({
|
||||||
|
onSuccessMutation,
|
||||||
|
onErrorMutation,
|
||||||
|
user,
|
||||||
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
|
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
|
||||||
|
const formMethods = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
username: currentUsername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={formMethods.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field: { ref, onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<UsernameAvailability
|
||||||
|
currentUsername={currentUsername}
|
||||||
|
setCurrentUsername={setCurrentUsername}
|
||||||
|
inputUsernameValue={value}
|
||||||
|
usernameRef={ref}
|
||||||
|
setInputUsernameValue={onChange}
|
||||||
|
onSuccessMutation={onSuccessMutation}
|
||||||
|
onErrorMutation={onErrorMutation}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect } from "react";
|
|||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
|
|
||||||
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
|
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
|
||||||
import { Swatch } from "@calcom/ui/v2";
|
import { Swatch } from "@calcom/ui";
|
||||||
|
|
||||||
type Handler = (event: MouseEvent | Event) => void;
|
type Handler = (event: MouseEvent | Event) => void;
|
||||||
function useEventListener<
|
function useEventListener<
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Props } from "react-select";
|
import { Props } from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
|
|||||||
import "react-date-picker/dist/DatePicker.css";
|
import "react-date-picker/dist/DatePicker.css";
|
||||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
// @see: https://github.com/wojtekmaj/react-daterange-picker/issues/91
|
|
||||||
import "@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
|
||||||
import PrimitiveDateRangePicker from "@wojtekmaj/react-daterange-picker/dist/entry.nostyle";
|
|
||||||
import React from "react";
|
|
||||||
import "react-calendar/dist/Calendar.css";
|
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
startDate: Date;
|
|
||||||
endDate: Date;
|
|
||||||
onDatesChange?: ((arg: { startDate: Date; endDate: Date }) => void) | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DateRangePicker = ({ startDate, endDate, onDatesChange }: Props) => {
|
|
||||||
return (
|
|
||||||
<PrimitiveDateRangePicker
|
|
||||||
className="rounded-sm border-gray-300 text-sm"
|
|
||||||
clearIcon={null}
|
|
||||||
calendarIcon={<Icon.FiCalendar className="h-5 w-5 text-gray-500" />}
|
|
||||||
rangeDivider={<Icon.FiArrowRight className="h-4 w-4 text-gray-400 ltr:mr-2 rtl:ml-2" />}
|
|
||||||
value={[startDate, endDate]}
|
|
||||||
onChange={([startDate, endDate]) => {
|
|
||||||
if (typeof onDatesChange === "function") onDatesChange({ startDate, endDate });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
|||||||
import type { DecorateProcedure } from "@calcom/trpc/react/shared";
|
import type { DecorateProcedure } from "@calcom/trpc/react/shared";
|
||||||
import type { AnyQueryProcedure, inferProcedureInput, inferProcedureOutput } from "@calcom/trpc/server";
|
import type { AnyQueryProcedure, inferProcedureInput, inferProcedureOutput } from "@calcom/trpc/server";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert, Loader } from "@calcom/ui";
|
||||||
import Loader from "@calcom/ui/Loader";
|
|
||||||
|
|
||||||
import type { UseTRPCQueryOptions } from "@trpc/react-query/shared";
|
import type { UseTRPCQueryOptions } from "@trpc/react-query/shared";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user