Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7911e9e2 | ||
|
|
cb09b5bc5f | ||
|
|
a99031bdd2 | ||
|
|
ada8a98362 | ||
|
|
e176e81bae | ||
|
|
b64dd66c2c | ||
|
|
7fd162601b | ||
|
|
c1f346c935 | ||
|
|
36d47bd2d8 | ||
|
|
26cdb6521f | ||
|
|
014f1986de | ||
|
|
3ef328421f | ||
|
|
c4fe69d826 | ||
|
|
6fd64b2209 | ||
|
|
585e36793a | ||
|
|
09b9370e13 | ||
|
|
a76a4996b4 | ||
|
|
b28d60b6dc | ||
|
|
a716eda14c | ||
|
|
99f1524183 | ||
|
|
7ce6454915 | ||
|
|
93cb21f55a | ||
|
|
0aca769d06 | ||
|
|
c0bda6257e | ||
|
|
ae22246f05 | ||
|
|
e2931d9fa2 | ||
|
|
e6a8a31b53 | ||
|
|
ee86c751f7 | ||
|
|
073b212c05 | ||
|
|
cd8e2124c4 | ||
|
|
9dd36bd20e | ||
|
|
b1404356ff | ||
|
|
f467bb7957 | ||
|
|
597b14cf4c | ||
|
|
2bdd6df755 | ||
|
|
ad958f10bd | ||
|
|
e5eb7c3906 | ||
|
|
667d42d5da | ||
|
|
a23a9ad15a | ||
|
|
4482ffc66b | ||
|
|
493efabd2e | ||
|
|
a0355e276d | ||
|
|
b5c993e153 | ||
|
|
da3ee3f1fe | ||
|
|
d9fc595444 | ||
|
|
06d13e0f6e | ||
|
|
7dc3266133 | ||
|
|
2c96444058 | ||
|
|
39c48e7f79 | ||
|
|
b6e1989f17 | ||
|
|
45b5b2a376 | ||
|
|
05726ec94c | ||
|
|
6340ff43d8 | ||
|
|
e64b26c7e1 | ||
|
|
c829a7de82 | ||
|
|
0205bfb066 | ||
|
|
5fd7d63388 | ||
|
|
e1ea2cb532 | ||
|
|
88d151aa35 | ||
|
|
4e7c93a322 | ||
|
|
281c7628c1 | ||
|
|
1b24bc14cd | ||
|
|
be3d2fca05 | ||
|
|
327e11e7c7 | ||
|
|
bd999345f4 | ||
|
|
081776b4f9 | ||
|
|
8c328ce5d7 | ||
|
|
0fa2a6aea3 | ||
|
|
440251f9c3 | ||
|
|
7f6847f7c7 | ||
|
|
9f1fa83a97 | ||
|
|
f00b112792 | ||
|
|
5339a489f5 | ||
|
|
f7df204e0b | ||
|
|
4975aba5e0 | ||
|
|
29d61ad67b | ||
|
|
6e555e64fa | ||
|
|
c748bbc7e6 | ||
|
|
0681359bf3 | ||
|
|
4c9d4d7972 | ||
|
|
67f8de42b3 | ||
|
|
87584132b5 | ||
|
|
5b943742e0 | ||
|
|
fe155b0b97 | ||
|
|
6639588c2d | ||
|
|
7f9e8ca7a6 | ||
|
|
e478a46358 | ||
|
|
7c9012738a | ||
|
|
5c763389f7 | ||
|
|
7fde30edf9 | ||
|
|
0e22e92cab | ||
|
|
17f48614ff | ||
|
|
2f33b57eb7 | ||
|
|
6e5c3a88ee | ||
|
|
9024914f23 | ||
|
|
966bc08eb4 | ||
|
|
c545331cfe | ||
|
|
ce763f30f6 | ||
|
|
d453ae9f2b | ||
|
|
9f2b41e597 | ||
|
|
86e903ce4a | ||
|
|
9d3963436b | ||
|
|
d8218a6243 | ||
|
|
cdc3833e09 | ||
|
|
52b7e401a1 | ||
|
|
5170fc2424 | ||
|
|
7349fb9f6d | ||
|
|
a8368aac79 | ||
|
|
91f381bce9 | ||
|
|
fb9c1b18e0 | ||
|
|
1d18a09cc8 | ||
|
|
887d63ceef | ||
|
|
555fcc21f2 | ||
|
|
f45fb03838 | ||
|
|
46611fdf77 | ||
|
|
c0f7c40bec | ||
|
|
f6f257c705 | ||
|
|
d61f50724b | ||
|
|
960aa0e00b | ||
|
|
817e20f11e | ||
|
|
af0123a606 | ||
|
|
709bbf6626 | ||
|
|
53097ee3bc | ||
|
|
31f2002654 | ||
|
|
ee155c3643 | ||
|
|
8ab20c3c9f | ||
|
|
4f6d035d29 | ||
|
|
2e5b42ef47 | ||
|
|
b055672ea0 | ||
|
|
e4fbca388c | ||
|
|
2b8bba09c9 | ||
|
|
98cf29f655 | ||
|
|
ae8706316f | ||
|
|
ec8e08d442 | ||
|
|
c33f424333 | ||
|
|
6f2312d1e2 | ||
|
|
c2b6070b7e | ||
|
|
a5bebe2b48 | ||
|
|
e3a1f20cc8 | ||
|
|
ab639591da | ||
|
|
21a9b64eb0 | ||
|
|
636ee7b8a1 | ||
|
|
543e526568 | ||
|
|
e465c1d71d | ||
|
|
cdab9037ce | ||
|
|
21e454d0e2 | ||
|
|
7384f29a1c | ||
|
|
9016a854ca | ||
|
|
446c29dd9c | ||
|
|
2cf1f07d8f | ||
|
|
4f03f8e66d | ||
|
|
e1096620e6 | ||
|
|
d416386d7b | ||
|
|
5b7c55b029 | ||
|
|
9b05311d7f | ||
|
|
313d25d7d8 | ||
|
|
5ec9c41f7c | ||
|
|
d56386d016 | ||
|
|
0968ede733 | ||
|
|
8e3ed28ba9 | ||
|
|
82ea4e9fb7 | ||
|
|
8a440c24c0 | ||
|
|
88a8da3ef6 | ||
|
|
49290fba72 | ||
|
|
a363b1e903 | ||
|
|
7b5e2c8bab | ||
|
|
4b0d22655d | ||
|
|
4f92f60d85 | ||
|
|
6986b5af40 | ||
|
|
e043a5efd8 | ||
|
|
bff28e5d6f | ||
|
|
f10ff18d9f | ||
|
|
cefe4754c7 | ||
|
|
d7aa3c7163 | ||
|
|
e899a92071 | ||
|
|
ebdbe407cd | ||
|
|
e7f917bab9 | ||
|
|
ba9688a04a | ||
|
|
f0e15d77dc | ||
|
|
30b72b687e | ||
|
|
bbf50dbc53 | ||
|
|
bb547a0111 | ||
|
|
1758288948 | ||
|
|
11789c725a | ||
|
|
73bfe0aebe | ||
|
|
24140f0f29 | ||
|
|
e504f5cd9e | ||
|
|
72629ffee0 | ||
|
|
5b6f007d8f | ||
|
|
2e0951d4dc | ||
|
|
fd84b5754d | ||
|
|
73afb03308 | ||
|
|
99dba73d0b | ||
|
|
db4449d849 | ||
|
|
53e52a0510 | ||
|
|
cf4bd8a77d | ||
|
|
9d61dc2f7c | ||
|
|
caf5918b72 | ||
|
|
84f64f5d06 | ||
|
|
13a53c020b | ||
|
|
a72f90fe02 | ||
|
|
283d5b4a65 | ||
|
|
81b2e37efd | ||
|
|
469967b320 | ||
|
|
d708625da0 | ||
|
|
acf2487eaa | ||
|
|
b81ca0e2ab | ||
|
|
2357f76cc8 | ||
|
|
304e8aaa68 | ||
|
|
9fe912c392 | ||
|
|
ab7ecff580 | ||
|
|
19e830acad | ||
|
|
458300bc83 | ||
|
|
352a467104 | ||
|
|
c052c66abc | ||
|
|
8f8ed4d80d | ||
|
|
04fb7e07b4 | ||
|
|
20d2129f81 | ||
|
|
ae5425f908 | ||
|
|
762810684e | ||
|
|
4720dc410f | ||
|
|
715aea5733 | ||
|
|
6cbb9bf967 | ||
|
|
d8176b2eb4 | ||
|
|
6c51e2a970 | ||
|
|
3a08eb3dca | ||
|
|
cee905bc5a | ||
|
|
72fd6334c6 | ||
|
|
63f51abd84 | ||
|
|
6e856b3bd1 | ||
|
|
5fceee27fe | ||
|
|
4f590ebbf7 | ||
|
|
736cd63509 | ||
|
|
3aaa1cde0d | ||
|
|
02d32321aa | ||
|
|
697f6db2c4 | ||
|
|
83364a9e97 | ||
|
|
1620ae660f | ||
|
|
605f9c61f3 | ||
|
|
564862369f | ||
|
|
be6cf4705c | ||
|
|
a2457ab619 | ||
|
|
867966dd75 | ||
|
|
08ad4c07ea | ||
|
|
78da57055e | ||
|
|
d5c3d9d967 | ||
|
|
aa0e9e6a02 | ||
|
|
cad97f1779 | ||
|
|
31ee689672 | ||
|
|
3edb6f5ffd | ||
|
|
a8825badec | ||
|
|
04c634ec4b | ||
|
|
8dd9b8d74b | ||
|
|
0365157638 | ||
|
|
a571d40958 | ||
|
|
69491d5700 | ||
|
|
20022e2cdd | ||
|
|
492858b45b | ||
|
|
b49ad8294b | ||
|
|
aa30ff7d1a | ||
|
|
cfa720a523 | ||
|
|
9ab196c30e | ||
|
|
47e13cd2fd | ||
|
|
74ab6d209e | ||
|
|
46f0b55a45 | ||
|
|
e4893c2218 | ||
|
|
fa17139e8f | ||
|
|
1cdc97dd37 | ||
|
|
f630e8af40 | ||
|
|
fe0745121b | ||
|
|
f6d7568d0a | ||
|
|
520e7fe036 | ||
|
|
1bf50e09cd | ||
|
|
d1ca7b4bdd | ||
|
|
4e2473531f | ||
|
|
efb9bf7116 | ||
|
|
49ee5d300b | ||
|
|
1512551903 | ||
|
|
1b459eb212 | ||
|
|
e4eb21bf8d | ||
|
|
79f7a16f62 | ||
|
|
7fc884912e | ||
|
|
20b3efbcb6 | ||
|
|
427b19238d | ||
|
|
696122a701 | ||
|
|
7d9c21cf9a | ||
|
|
eb93524f49 | ||
|
|
07fd3ad29f | ||
|
|
389633394b | ||
|
|
d27817ad51 | ||
|
|
7af23305eb | ||
|
|
db947591ad | ||
|
|
2ef4a05750 | ||
|
|
f1ac45ee19 | ||
|
|
03c9fc7952 | ||
|
|
de2a56ebcb | ||
|
|
0bae688da1 | ||
|
|
7967a65ade | ||
|
|
1cd96d4c25 | ||
|
|
19bc329377 | ||
|
|
0c29f2a135 | ||
|
|
a145de56ee | ||
|
|
d2352da54b | ||
|
|
5d7685f2c0 | ||
|
|
ee8b2e67f0 | ||
|
|
a0c9123ee7 | ||
|
|
131611e114 | ||
|
|
b3407e19b5 | ||
|
|
0d4b0847d4 | ||
|
|
5d8575abf4 | ||
|
|
631bf4d9fe | ||
|
|
8fd1d76227 | ||
|
|
22b189cb19 | ||
|
|
27c29c65e2 | ||
|
|
f0c7d7ff29 | ||
|
|
679965782e | ||
|
|
650ecc7a85 | ||
|
|
8d27dcdea3 | ||
|
|
6128be8860 | ||
|
|
13577a5dbe | ||
|
|
9ba13a3f3c | ||
|
|
9ac562cfba | ||
|
|
e1411b3fb2 | ||
|
|
0f89415ec3 | ||
|
|
c6ff3dca4a | ||
|
|
e001c0b88f | ||
|
|
5cbd334a42 | ||
|
|
470ef737b5 | ||
|
|
f3e8bed1cf | ||
|
|
750362c0cf | ||
|
|
3811dd564d | ||
|
|
99fe67b879 | ||
|
|
2685e8154f | ||
|
|
4308ea8d52 | ||
|
|
358aa8be2b | ||
|
|
b20c0df4e8 | ||
|
|
09af14b3f4 | ||
|
|
77df4117d0 | ||
|
|
84f7924215 | ||
|
|
c8554e6878 | ||
|
|
5993e0842c | ||
|
|
7179b09d7b | ||
|
|
325dc9805f | ||
|
|
0f6a678b74 |
No files matched your search
@@ -20,12 +20,17 @@
|
|||||||
# - 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 ⚠️ ⚠️ ⚠️
|
||||||
# - DAILY.CO VIDEO
|
# - DAILY.CO VIDEO
|
||||||
|
# Enables Cal Video. to get your key
|
||||||
|
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||||
|
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
|
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
|
||||||
|
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
|
||||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
# Needed to enable Google Calendar integration and Login with Google
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
|
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||||
GOOGLE_API_CREDENTIALS=
|
GOOGLE_API_CREDENTIALS=
|
||||||
|
|
||||||
# To enable Login with Google you need to:
|
# To enable Login with Google you need to:
|
||||||
|
|||||||
+8
-6
@@ -22,10 +22,13 @@ CALCOM_LICENSE_KEY=
|
|||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
# Opt-in to a dedicated connection pool for Prisma using Prisma Data Proxy
|
|
||||||
# Cold boots will be faster and you will be able to scale your DB independently of your application.
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||||
PRISMA_GENERATE_DATAPROXY=false
|
# PRISMA_GENERATE_DATAPROXY=true
|
||||||
|
PRISMA_GENERATE_DATAPROXY=
|
||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
@@ -157,9 +160,6 @@ CLOSECOM_API_KEY=
|
|||||||
# Sendgrid internal sync service
|
# Sendgrid internal sync service
|
||||||
SENDGRID_SYNC_API_KEY=
|
SENDGRID_SYNC_API_KEY=
|
||||||
|
|
||||||
# Sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
|
||||||
|
|
||||||
# Change your Brand
|
# Change your Brand
|
||||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
@@ -174,3 +174,5 @@ CSP_POLICY=
|
|||||||
|
|
||||||
# Vercel Edge Config
|
# Vercel Edge Config
|
||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
# the repo. Unless a later match takes precedence,
|
# the repo. Unless a later match takes precedence,
|
||||||
# @global-owner1 and @global-owner2 will be requested for
|
# @global-owner1 and @global-owner2 will be requested for
|
||||||
# review when someone opens a pull request.
|
# review when someone opens a pull request.
|
||||||
* @calcom/reviewers
|
# ⬇️ Removed so we can manually trigger checks
|
||||||
|
# * @calcom/reviewers
|
||||||
|
|
||||||
.github/ @calcom/core
|
.github/ @calcom/core
|
||||||
.husky/ @calcom/core
|
.husky/ @calcom/core
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete options that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "tsc-absolute",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: "Apply issue labels to PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label_on_pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: Apply labels from linked issue to PR
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
||||||
|
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
||||||
@@ -10,8 +10,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
- name: Configure TSC problem matcher
|
||||||
- run: yarn type-check
|
run: |
|
||||||
|
echo "::remove-matcher owner=tsc::"
|
||||||
|
echo "::add-matcher::.github/matchers/tsc-absolute.json"
|
||||||
|
- run: yarn type-check:ci
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
name: Cron - downgradeUsers
|
name: Cron - downgradeUsers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
schedule:
|
schedule:
|
||||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
# Runs “Every month at 1st (see https://crontab.guru)
|
||||||
- cron: "0,15,30,45 * * * *"
|
- cron: "0 0 1 * *"
|
||||||
jobs:
|
jobs:
|
||||||
cron-downgradeUsers:
|
cron-downgradeUsers:
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
days-before-stale: 60
|
days-before-issue-stale: 60
|
||||||
include-only-assigned: true
|
days-before-issue-close: -1
|
||||||
days-before-close: -1
|
days-before-pr-stale: 14
|
||||||
|
days-before-pr-close: 7
|
||||||
|
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||||
|
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||||
@@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@1.5.1
|
uses: crowdin/github-action@1.5.1
|
||||||
@@ -26,6 +28,6 @@ jobs:
|
|||||||
localization_branch_name: main
|
localization_branch_name: main
|
||||||
create_pull_request: false
|
create_pull_request: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Analyze bundle
|
- name: Analyze bundle
|
||||||
run: |
|
run: |
|
||||||
cd apps/web
|
cd apps/web
|
||||||
npx -p nextjs-bundle-analysis report
|
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||||
|
|
||||||
- name: Create Comment
|
- name: Create Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
||||||
|
|
||||||
|
// Get the labels from issues linked to the PR
|
||||||
|
const query = `
|
||||||
|
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const graphqlData = JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: {
|
||||||
|
owner: pr.base.repo.owner.login,
|
||||||
|
repo: pr.base.repo.name,
|
||||||
|
prNumber: pr.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: "/graphql",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": graphqlData.length,
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the native Node.js request library to make the request
|
||||||
|
let linkedIssues;
|
||||||
|
linkedIssues = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(
|
||||||
|
JSON.parse(responseBody)
|
||||||
|
?.data
|
||||||
|
?.repository
|
||||||
|
?.pullRequest
|
||||||
|
?.closingIssuesReferences
|
||||||
|
?.nodes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(graphqlData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkedIssues || linkedIssues.length === 0) {
|
||||||
|
console.log("No issue linked.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over linked issues and apply labels to PR
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
||||||
|
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
console.log(`No labels found on the linked issue #${issue.number}.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsData = JSON.stringify({
|
||||||
|
labels: labels,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": labelsData.length,
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let labelResult;
|
||||||
|
labelResult = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("data", () => {});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(JSON.parse(responseBody));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(labelsData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (labelResult.message) {
|
||||||
|
console.log(`Error labelling PR: ${labelResult.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Applied labels: ${labels.join(", ")} to PR #${
|
||||||
|
pr.number
|
||||||
|
} from linked issue #${issue.number}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the PR data and GitHub token to the script
|
||||||
|
(async () => {
|
||||||
|
if (!process.env.PR_DATA) {
|
||||||
|
console.log("No PR data found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = JSON.parse(process.env.PR_DATA);
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
|
await applyLabelFromLinkedIssueToPR(prData, token);
|
||||||
|
})();
|
||||||
@@ -88,6 +88,7 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
- [React](https://reactjs.org/?ref=cal.com)
|
- [React](https://reactjs.org/?ref=cal.com)
|
||||||
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
||||||
- [Prisma](https://prisma.io/?ref=cal.com)
|
- [Prisma](https://prisma.io/?ref=cal.com)
|
||||||
|
- [Daily.co](https://go.cal.com/daily)
|
||||||
|
|
||||||
## Stay Up-to-Date
|
## Stay Up-to-Date
|
||||||
|
|
||||||
@@ -349,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
||||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTORS -->
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ following
|
|||||||
|
|
||||||
### Obtaining Daily API Credentials
|
### Obtaining Daily API Credentials
|
||||||
|
|
||||||
1. Open [Daily](https://www.daily.co/) and sign into your account.
|
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||||
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
3. Copy your API key.
|
3. Copy your API key.
|
||||||
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
||||||
@@ -455,12 +455,12 @@ following
|
|||||||
|
|
||||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||||
2. From within the API console page, go to "Applications".
|
2. From within the API console page, go to "Applications".
|
||||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||||
4. Fill in any information you want in the "Client Details" tab
|
4. Fill in any information you want in the "Client Details" tab
|
||||||
5. Go to tab "Client Secret" tab.
|
5. Go to tab "Client Secret" tab.
|
||||||
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/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/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||||
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||||
"start": "PORT=4000 next start",
|
"start": "PORT=4000 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"author": "Cal.com, Inc.",
|
"author": "Cal.com, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export function VariantsTable({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!isDark && (
|
{!isDark && (
|
||||||
<div data-mode="dark">
|
<div data-mode="dark" className="dark">
|
||||||
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
||||||
{children}
|
{children}
|
||||||
</VariantsTable>
|
</VariantsTable>
|
||||||
|
|||||||
@@ -193,3 +193,88 @@
|
|||||||
.docs-story {
|
.docs-story {
|
||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer {
|
||||||
|
:root {
|
||||||
|
/* background */
|
||||||
|
|
||||||
|
--cal-bg-emphasis: #E5E7EB;
|
||||||
|
--cal-bg: white;
|
||||||
|
--cal-bg-subtle: #F3F4F6;
|
||||||
|
--cal-bg-muted: #F9FAFB;
|
||||||
|
--cal-bg-inverted: #111827;
|
||||||
|
|
||||||
|
/* background -> components*/
|
||||||
|
--cal-bg-info: #DEE9FC;
|
||||||
|
--cal-bg-success: #E2FBE8;
|
||||||
|
--cal-bg-attention: #FCEED8;
|
||||||
|
--cal-bg-error: #F9E3E2;
|
||||||
|
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--cal-border-emphasis:#9CA3AF;
|
||||||
|
--cal-border: #D1D5DB;
|
||||||
|
--cal-border-subtle:#E5E7EB;
|
||||||
|
--cal-border-muted:#F3F4F6;
|
||||||
|
|
||||||
|
/* Content/Text */
|
||||||
|
--cal-text-emphasis: #111827;
|
||||||
|
--cal-text:#374151;
|
||||||
|
--cal-text-subtle:#6B7280;
|
||||||
|
--cal-text-muted:#9CA3AF;
|
||||||
|
--cal-text-inverted:white;
|
||||||
|
|
||||||
|
/* Content/Text -> components */
|
||||||
|
--cal-text-info:#253985;
|
||||||
|
--cal-text-success:#285231;
|
||||||
|
--cal-text-attention:#73321B;
|
||||||
|
--cal-text-error:#752522;
|
||||||
|
|
||||||
|
/* Brand shinanigans
|
||||||
|
-> These will be computed for the users theme at runtime.
|
||||||
|
*/
|
||||||
|
--cal-brand:#111827;
|
||||||
|
--cal-brand-emphasis:#101010;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
/* background */
|
||||||
|
|
||||||
|
--cal-bg-emphasis: #2B2B2B;
|
||||||
|
--cal-bg: #101010;
|
||||||
|
--cal-bg-subtle: #2B2B2B;
|
||||||
|
--cal-bg-muted: #1C1C1C;
|
||||||
|
--cal-bg-inverted: #F3F4F6;
|
||||||
|
|
||||||
|
/* background -> components*/
|
||||||
|
--cal-bg-info: #DEE9FC;
|
||||||
|
--cal-bg-success: #E2FBE8;
|
||||||
|
--cal-bg-attention: #FCEED8;
|
||||||
|
--cal-bg-error: #F9E3E2;
|
||||||
|
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--cal-border-emphasis: #575757;
|
||||||
|
--cal-border: #444444;
|
||||||
|
--cal-border-subtle: #2B2B2B;
|
||||||
|
--cal-border-muted: #1C1C1C;
|
||||||
|
|
||||||
|
/* Content/Text */
|
||||||
|
--cal-text-emphasis: #F3F4F6;
|
||||||
|
--cal-text: #D6D6D6;
|
||||||
|
--cal-text-subtle: #757575;
|
||||||
|
--cal-text-muted: #575757;
|
||||||
|
--cal-text-inverted: #101010;
|
||||||
|
|
||||||
|
/* Content/Text -> components */
|
||||||
|
--cal-text-info: #253985;
|
||||||
|
--cal-text-success: #285231;
|
||||||
|
--cal-text-attention: #73321B;
|
||||||
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
|
/* Brand shenanigans
|
||||||
|
-> These will be computed for the users theme at runtime.
|
||||||
|
*/
|
||||||
|
--cal-brand: #111827;
|
||||||
|
--cal-brand-emphasis: #101010;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,5 +68,3 @@ public/embed
|
|||||||
|
|
||||||
# Copied app-store images
|
# Copied app-store images
|
||||||
public/app-store
|
public/app-store
|
||||||
# Sentry
|
|
||||||
.sentryclirc
|
|
||||||
@@ -103,59 +103,6 @@ paths:
|
|||||||
summary: Reset a user's password
|
summary: Reset a user's password
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
|
|
||||||
get:
|
|
||||||
description: "Gets the busy times for a particular user, by username."
|
|
||||||
summary: Gets the busy times for a user
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
description: ""
|
|
||||||
minItems: 1
|
|
||||||
uniqueItems: true
|
|
||||||
x-examples:
|
|
||||||
example-1:
|
|
||||||
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
|
|
||||||
end: "Fri, 03 Sep 2021 17:40:00 GMT"
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
start:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
end:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
|
||||||
- start
|
|
||||||
- end
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
parameters:
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: user
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The username of who you want to check availability for
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateFrom
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times from
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateTo
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times until
|
|
||||||
/api/availability/calendar:
|
/api/availability/calendar:
|
||||||
get:
|
get:
|
||||||
description: Gets the user's selected calendars.
|
description: Gets the user's selected calendars.
|
||||||
|
|||||||
@@ -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 { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -29,7 +29,7 @@ export default function AddToHomescreen() {
|
|||||||
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
|
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs font-medium text-white ltr:ml-3 rtl:mr-3">
|
<p className="text-inverted ms-3 text-xs font-medium">
|
||||||
<span className="inline">{t("add_to_homescreen")}</span>
|
<span className="inline">{t("add_to_homescreen")}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<span className="sr-only">{t("dismiss")}</span>
|
<span className="sr-only">{t("dismiss")}</span>
|
||||||
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
<X className="text-inverted h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
interface AdditionalCalendarSelectorProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { data } = optionProps;
|
|
||||||
return (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="w-full" color="minimal">
|
|
||||||
{/* eslint-disable @next/next/no-img-element */}
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p>{data.label}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => {
|
|
||||||
const options = data.items.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
slug: item.slug,
|
|
||||||
image: item.logo,
|
|
||||||
type: item.type,
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="additionalCalendar"
|
|
||||||
placeholder={t("connect_additional_calendar")}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
color: "#3E3E3E",
|
|
||||||
marginLeft: "3px",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "2px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
maxWidth: "320px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
|
||||||
isLoading={isLoading}
|
|
||||||
components={{ Option: ImageOption }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdditionalCalendarSelector;
|
|
||||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { Badge, ListItemText } from "@calcom/ui";
|
import { Badge, ListItemText } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
|
||||||
<div className="flex gap-x-3 px-5 py-4">
|
<div className="flex gap-x-3 px-5 py-4">
|
||||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||||
<div className="flex grow flex-col gap-y-1 truncate">
|
<div className="flex grow flex-col gap-y-1 truncate">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||||
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
<ListItemText component="p">{description}</ListItemText>
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
{invalidCredential && (
|
{invalidCredential && (
|
||||||
<div className="flex gap-x-2 pt-2">
|
<div className="flex gap-x-2 pt-2">
|
||||||
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
{t("invalid_credential")}
|
{t("invalid_credential")}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: selected.name || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: cal.name || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
option: (defaultStyles, state) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
backgroundColor: state.isSelected
|
|
||||||
? state.isFocused
|
|
||||||
? "var(--brand-color)"
|
|
||||||
: "var(--brand-color)"
|
|
||||||
: state.isFocused
|
|
||||||
? "var(--brand-color-dark-mode)"
|
|
||||||
: "var(--brand-text-color)",
|
|
||||||
}),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
+182
-182
@@ -1,4 +1,4 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
|||||||
const height = getDimension(previewState.inline.height);
|
const height = getDimension(previewState.inline.height);
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
|||||||
}) => {
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi();
|
||||||
Cal("floatingButton", ${floatingButtonArg});
|
cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
|||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -313,7 +313,7 @@ const embeds: {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
title: "Inline Embed",
|
title: "Inline Embed",
|
||||||
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
|
subtitle: "Loads your event type directly inline with your other website content.",
|
||||||
type: "inline",
|
type: "inline",
|
||||||
illustration: (
|
illustration: (
|
||||||
<svg
|
<svg
|
||||||
@@ -327,10 +327,10 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
||||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
|
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
|
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
@@ -378,9 +378,9 @@ const embeds: {
|
|||||||
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
|
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
||||||
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
|
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -400,12 +400,12 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
|
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
||||||
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
@@ -426,62 +426,65 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<g filter="url(#filter0_i_3223_14162)">
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
|
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
|
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
|
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
</g>
|
||||||
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
|
||||||
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<path
|
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
|
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
fill="#3E3E3E"
|
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
/>
|
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
|
||||||
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
|
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
|
||||||
|
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -490,7 +493,7 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
href: "embedTabName=embed-code",
|
href: "embedTabName=embed-code",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -506,7 +509,7 @@ const tabs = [
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<small className="flex py-4 text-gray-500">
|
<small className="text-subtle flex py-4">
|
||||||
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -514,7 +517,7 @@ const tabs = [
|
|||||||
data-testid="embed-code"
|
data-testid="embed-code"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-code"
|
name="embed-code"
|
||||||
className="h-[calc(100%-50px)] font-mono"
|
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
|
||||||
style={{ resize: "none", overflow: "auto" }}
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
@@ -531,7 +534,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
<!-- Cal ${embedType} embed code ends -->`
|
<!-- Cal ${embedType} embed code ends -->`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<p className="hidden text-sm text-gray-500">
|
<p className="text-subtle hidden text-sm">
|
||||||
{t(
|
{t(
|
||||||
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
||||||
)}
|
)}
|
||||||
@@ -543,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
{
|
{
|
||||||
name: "React",
|
name: "React",
|
||||||
href: "embedTabName=embed-react",
|
href: "embedTabName=embed-react",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -558,12 +561,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
|
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="embed-react"
|
data-testid="embed-react"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-react"
|
name="embed-react"
|
||||||
className="h-[calc(100%-50px)] font-mono"
|
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
|
||||||
readOnly
|
readOnly
|
||||||
style={{ resize: "none", overflow: "auto" }}
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
value={`/* First make sure that you have installed the package */
|
value={`/* First make sure that you have installed the package */
|
||||||
@@ -583,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
{
|
{
|
||||||
name: "Preview",
|
name: "Preview",
|
||||||
href: "embedTabName=embed-preview",
|
href: "embedTabName=embed-preview",
|
||||||
icon: FiTrello,
|
icon: Trello,
|
||||||
type: "iframe",
|
type: "iframe",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||||
@@ -619,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
|||||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||||
return (
|
return (
|
||||||
<components.Control {...props}>
|
<components.Control {...props}>
|
||||||
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
<Sun className="text-subtle ml-2 h-4 w-4" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -629,19 +632,19 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<DialogContent type="creation" size="lg">
|
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||||
<div className="mb-4">
|
<div className="mb-2">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-emphasis mb-2 text-2xl font-bold leading-none" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
{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", { appName: APP_NAME })}</p>
|
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="items-start space-y-2 md:flex md:space-y-0">
|
||||||
{embeds.map((embed, index) => (
|
{embeds.map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
|
className="hover:bg-subtle bg-muted w-full rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -649,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
embedType: embed.type,
|
embedType: embed.type,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
<div className="bg-default order-none box-border flex-none rounded-md border border-solid dark:bg-transparent dark:invert">
|
||||||
{embed.illustration}
|
{embed.illustration}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
|
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
|
||||||
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -795,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeOptions = [
|
const ThemeOptions = [
|
||||||
{ value: Theme.auto, label: "Auto Theme" },
|
{ value: Theme.auto, label: "Auto" },
|
||||||
{ value: Theme.dark, label: "Dark Theme" },
|
{ value: Theme.dark, label: "Dark Theme" },
|
||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
@@ -812,67 +815,62 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
<DialogContent
|
||||||
|
ref={dialogContentRef}
|
||||||
|
className="rounded-lg p-0.5 sm:max-w-[80rem]"
|
||||||
|
enableOverflow
|
||||||
|
type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="bg-muted flex w-1/3 flex-col p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3
|
||||||
|
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
|
||||||
|
id="modal-title">
|
||||||
<button
|
<button
|
||||||
|
className="h-6 w-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
<FiArrowLeft className="mr-4 w-4" />
|
<ArrowLeft className="mr-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<h4 className="text-emphasis mb-6 text-sm font-normal">{embed.subtitle}</h4>
|
||||||
<div className="flex flex-col overflow-y-auto">
|
<div className="flex flex-col">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center text-base font-medium text-gray-900">
|
|
||||||
<div>
|
|
||||||
{embedType === "inline"
|
|
||||||
? "Inline Embed Customization"
|
|
||||||
: embedType === "floating-popup"
|
|
||||||
? "Floating Popup Customization"
|
|
||||||
: "Element Click Customization"}
|
|
||||||
</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="text-sm">
|
<CollapsibleContent className="text-sm">
|
||||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="text-default mb-[9px] text-sm">Window sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center !font-normal">
|
||||||
<TextField
|
<div className="mr-[9px]">
|
||||||
labelProps={{ className: "hidden" }}
|
<TextField
|
||||||
required
|
labelProps={{ className: "hidden" }}
|
||||||
value={previewState.inline.width}
|
className="focus:ring-offset-0"
|
||||||
onChange={(e) => {
|
required
|
||||||
setPreviewState((previewState) => {
|
value={previewState.inline.width}
|
||||||
const width = e.target.value || "100%";
|
onChange={(e) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
const width = e.target.value || "100%";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
inline: {
|
||||||
|
...previewState.inline,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
addOnLeading={<>W</>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
return {
|
|
||||||
...previewState,
|
|
||||||
inline: {
|
|
||||||
...previewState.inline,
|
|
||||||
width,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
addOnLeading={<>W</>}
|
|
||||||
/>
|
|
||||||
<span className="p-2">×</span>
|
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
|
className="focus:ring-offset-0"
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -895,7 +893,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "" : "hidden"
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<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 */}
|
||||||
@@ -919,7 +917,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 flex items-center justify-start",
|
"mt-4 flex items-center justify-start",
|
||||||
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
|
embedType === "floating-popup"
|
||||||
|
? "text-emphasis space-x-2 rtl:space-x-reverse"
|
||||||
|
: "hidden"
|
||||||
)}>
|
)}>
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
@@ -935,12 +935,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm">Display Calendar Icon Button</div>
|
<div className="text-default text-sm">Display Calendar Icon Button</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "" : "hidden"
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-2">Position of Button</div>
|
<div className="mb-2">Position of Button</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -959,7 +959,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={FloatingPopupPositionOptions}
|
options={FloatingPopupPositionOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4",
|
||||||
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
|
)}>
|
||||||
<div>Button Color</div>
|
<div>Button Color</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@@ -980,7 +984,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"mt-4",
|
||||||
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
|
)}>
|
||||||
<div>Text Color</div>
|
<div>Text Color</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@@ -1004,22 +1012,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<div className="font-medium">
|
||||||
<div className="mt-4 font-medium">
|
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
|
||||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</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 rtl:space-x-reverse">
|
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
<Switch
|
<Switch
|
||||||
checked={previewState.hideEventTypeDetails}
|
checked={previewState.hideEventTypeDetails}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -1031,15 +1030,40 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
|
||||||
</div>
|
</div>
|
||||||
<Label className="">
|
{[
|
||||||
|
{ name: "brandColor", title: "Brand Color" },
|
||||||
|
// { name: "lightColor", title: "Light Color" },
|
||||||
|
// { name: "lighterColor", title: "Lighter Color" },
|
||||||
|
// { name: "lightestColor", title: "Lightest Color" },
|
||||||
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
|
// { name: "medianColor", title: "Median Color" },
|
||||||
|
].map((palette) => (
|
||||||
|
<Label key={palette.name} className="mb-6">
|
||||||
|
<div className="mb-2">{palette.title}</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<ColorPicker
|
||||||
|
popoverAlign="start"
|
||||||
|
container={dialogContentRef?.current ?? undefined}
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
addToPalette({
|
||||||
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
<Label>
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={ThemeOptions[0]}
|
defaultValue={ThemeOptions[0]}
|
||||||
components={{
|
components={{
|
||||||
Control: ThemeSelectControl,
|
Control: ThemeSelectControl,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
}}
|
}}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
@@ -1055,30 +1079,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={ThemeOptions}
|
options={ThemeOptions}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{[
|
|
||||||
{ name: "brandColor", title: "Brand Color" },
|
|
||||||
// { name: "lightColor", title: "Light Color" },
|
|
||||||
// { name: "lighterColor", title: "Lighter Color" },
|
|
||||||
// { name: "lightestColor", title: "Lightest Color" },
|
|
||||||
// { name: "highlightColor", title: "Highlight Color" },
|
|
||||||
// { name: "medianColor", title: "Median Color" },
|
|
||||||
].map((palette) => (
|
|
||||||
<Label key={palette.name} className="pb-4">
|
|
||||||
<div className="mb-2 pt-2">{palette.title}</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<ColorPicker
|
|
||||||
popoverAlign="start"
|
|
||||||
container={dialogContentRef?.current ?? undefined}
|
|
||||||
defaultValue="#000000"
|
|
||||||
onChange={(color) => {
|
|
||||||
addToPalette({
|
|
||||||
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function ImageUploader({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-emphasis text-lg leading-6" id="modal-title">
|
||||||
{t("upload_target", { target })}
|
{t("upload_target", { target })}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,9 +131,9 @@ export default function ImageUploader({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
|
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
|
||||||
{!result && (
|
{!result && (
|
||||||
<div className="flex h-20 max-h-20 w-20 items-center justify-start rounded-full bg-gray-50">
|
<div className="bg-muted flex h-20 max-h-20 w-20 items-center justify-start rounded-full">
|
||||||
{!imageSrc && (
|
{!imageSrc && (
|
||||||
<p className="w-full text-center text-sm text-white sm:text-xs">
|
<p className="text-inverted w-full text-center text-sm sm:text-xs">
|
||||||
{t("no_target", { target })}
|
{t("no_target", { target })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -144,7 +144,7 @@ export default function ImageUploader({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
<label className="border-default bg-default hover:bg-mutedover:text-emphasis dark:hover:bg-inverted text-default dark:text-inverted focus:ring-empthasis mt-8 rounded-sm border px-3 py-1 text-xs font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent">
|
||||||
<input
|
<input
|
||||||
onInput={onInputFile}
|
onInput={onInputFile}
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import { noop } from "lodash";
|
||||||
import type { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -69,8 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-gray-900 text-gray-900"
|
? "text-emphasis border-gray-900"
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
: "hover:border-default hover:text-default text-subtle border-transparent",
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -78,7 +78,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
{tab.icon && (
|
{tab.icon && (
|
||||||
<tab.icon
|
<tab.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
|
||||||
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -91,7 +91,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { DefaultSeo } from "next-seo";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import { useEmbedUiConfig } from "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
import AppProviders from "@lib/app-providers";
|
||||||
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
|
export interface CalPageWrapper {
|
||||||
|
(props?: any): JSX.Element;
|
||||||
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
|
const calFont = localFont({
|
||||||
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
|
variable: "--font-cal",
|
||||||
|
preload: true,
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageWrapper(props: AppProps) {
|
||||||
|
const { Component, pageProps, err, router } = props;
|
||||||
|
let pageStatus = "200";
|
||||||
|
const { cssVarsPerTheme } = useEmbedUiConfig();
|
||||||
|
const cssVarsStyle = [];
|
||||||
|
if (cssVarsPerTheme) {
|
||||||
|
for (const [themeName, cssVars] of Object.entries(cssVarsPerTheme)) {
|
||||||
|
cssVarsStyle.push(`.${themeName} {`);
|
||||||
|
for (const [cssVarName, value] of Object.entries(cssVars)) {
|
||||||
|
cssVarsStyle.push(`--${cssVarName}: ${value};`);
|
||||||
|
}
|
||||||
|
cssVarsStyle.push(`}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (router.pathname === "/404") {
|
||||||
|
pageStatus = "404";
|
||||||
|
} else if (router.pathname === "/500") {
|
||||||
|
pageStatus = "500";
|
||||||
|
}
|
||||||
|
|
||||||
|
// On client side don't let nonce creep into DOM
|
||||||
|
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||||
|
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||||
|
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
||||||
|
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
||||||
|
const providerProps = {
|
||||||
|
...props,
|
||||||
|
pageProps: {
|
||||||
|
...props.pageProps,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Use the layout defined at the page level, if available
|
||||||
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
return (
|
||||||
|
<AppProviders {...providerProps}>
|
||||||
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
nonce={nonce}
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-inter: ${interFont.style.fontFamily};
|
||||||
|
--font-cal: ${calFont.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<style jsx global>
|
||||||
|
{`
|
||||||
|
${cssVarsStyle.join("")}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
{getLayout(
|
||||||
|
Component.requiresLicense ? (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
),
|
||||||
|
router
|
||||||
|
)}
|
||||||
|
</AppProviders>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageWrapper;
|
||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
@@ -11,32 +11,32 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/my-account/profile",
|
href: "/settings/my-account/profile",
|
||||||
icon: FiUser,
|
icon: User,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teams",
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "security",
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: FiKey,
|
icon: Key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "developer",
|
name: "developer",
|
||||||
href: "/settings/developer",
|
href: "/settings/developer",
|
||||||
icon: FiTerminal,
|
icon: Terminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "billing",
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: FiLock,
|
icon: Lock,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { 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 {
|
||||||
import { Button, Select } from "@calcom/ui";
|
Button,
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { data } = optionProps;
|
|
||||||
return data.slug !== "add-new" ? (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
|
||||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
|
||||||
<p>{t("install_new_calendar_app")}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||||
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
type: "new_other",
|
type: "new_other",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Select
|
<Dropdown modal={false}>
|
||||||
name="additionalCalendar"
|
<DropdownMenuTrigger asChild>
|
||||||
placeholder={
|
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||||
<Button StartIcon={FiPlus} color="secondary">
|
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</DropdownMenuTrigger>
|
||||||
options={options}
|
<DropdownMenuContent>
|
||||||
isSearchable={false}
|
{options.map((data) => (
|
||||||
isLoading={isLoading}
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
components={{ Option: ImageOption }}
|
{data.slug === "add-new" ? (
|
||||||
styles={{
|
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
||||||
menu: (defaultStyles) => ({
|
{t("install_new_calendar_app")}
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
"@media only screen and (max-width: 640px)": {
|
) : (
|
||||||
left: "0",
|
<InstallAppButton
|
||||||
},
|
type={data.type}
|
||||||
width: "max-content",
|
render={(installProps) => {
|
||||||
right: "0",
|
const props = { ...installProps } as any;
|
||||||
}),
|
return (
|
||||||
indicatorSeparator: (defaultStyles) => ({
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
...defaultStyles,
|
<span className="flex items-center gap-x-2">
|
||||||
display: "none",
|
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
|
||||||
}),
|
{`${t("add")} ${data.label}`}
|
||||||
control: (defaultStyles) => ({
|
</span>
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
padding: "0",
|
);
|
||||||
border: "0",
|
}}
|
||||||
borderRadius: "6px",
|
/>
|
||||||
minHeight: "auto",
|
)}
|
||||||
}),
|
</DropdownMenuItem>
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
))}
|
||||||
...defaultStyles,
|
</DropdownMenuContent>
|
||||||
display: "none",
|
</Dropdown>
|
||||||
}),
|
|
||||||
valueContainer: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
padding: "0",
|
|
||||||
}),
|
|
||||||
placeholder: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
margin: "0",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
@@ -13,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
import {
|
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
||||||
FiBookOpen,
|
|
||||||
FiCheck,
|
|
||||||
FiExternalLink,
|
|
||||||
FiFile,
|
|
||||||
FiFlag,
|
|
||||||
FiMail,
|
|
||||||
FiPlus,
|
|
||||||
FiShield,
|
|
||||||
} from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
/* These app slugs all require Google Cal to be installed */
|
/* These app slugs all require Google Cal to be installed */
|
||||||
|
|
||||||
@@ -45,12 +37,12 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
images,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const hasImages = images && images.length > 0;
|
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
@@ -94,17 +86,25 @@ const Component = ({
|
|||||||
|
|
||||||
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">
|
||||||
{hasImages && (
|
{hasDescriptionItems && (
|
||||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{images ? (
|
{descriptionItems ? (
|
||||||
images.map((img) => (
|
descriptionItems.map((descriptionItem, index) =>
|
||||||
<img
|
typeof descriptionItem === "object" ? (
|
||||||
key={img}
|
<div
|
||||||
src={img}
|
key={`iframe-${index}`}
|
||||||
alt={`Screenshot of app ${name}`}
|
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
|
||||||
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
<iframe allowFullScreen {...descriptionItem.iframe} />
|
||||||
/>
|
</div>
|
||||||
))
|
) : (
|
||||||
|
<img
|
||||||
|
key={descriptionItem}
|
||||||
|
src={descriptionItem}
|
||||||
|
alt={`Screenshot of app ${name}`}
|
||||||
|
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText />
|
<SkeletonText />
|
||||||
)}
|
)}
|
||||||
@@ -113,18 +113,22 @@ const Component = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||||
hasImages && "lg:ml-8"
|
hasDescriptionItems && "lg:ml-8"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 flex pt-4">
|
<div className="mb-8 flex pt-4">
|
||||||
<header>
|
<header>
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-4 flex items-center">
|
||||||
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} />
|
<img
|
||||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
|
||||||
|
src={logo}
|
||||||
|
alt={name}
|
||||||
|
/>
|
||||||
|
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm font-medium text-gray-600">
|
<h2 className="text-default text-sm font-medium">
|
||||||
<Link
|
<Link
|
||||||
href={`categories/${categories[0]}`}
|
href={`categories/${categories[0]}`}
|
||||||
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
|
||||||
{categories[0]}
|
{categories[0]}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
• {t("published_by", { author })}
|
• {t("published_by", { author })}
|
||||||
@@ -140,7 +144,7 @@ const Component = ({
|
|||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
<Button StartIcon={Check} color="secondary" disabled>
|
||||||
{existingCredentials.length > 0
|
{existingCredentials.length > 0
|
||||||
? t("active_install", { count: existingCredentials.length })
|
? t("active_install", { count: existingCredentials.length })
|
||||||
: t("default")}
|
: t("default")}
|
||||||
@@ -162,7 +166,7 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
{...props}
|
{...props}
|
||||||
// @TODO: Overriding color and size prevent us from
|
// @TODO: Overriding color and size prevent us from
|
||||||
// having to duplicate InstallAppButton for now.
|
// having to duplicate InstallAppButton for now.
|
||||||
@@ -234,9 +238,11 @@ const Component = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="prose-sm prose mt-8">{body}</div>
|
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
{body}
|
||||||
<span>
|
</div>
|
||||||
|
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||||
|
<span className="text-default">
|
||||||
{price === 0 ? (
|
{price === 0 ? (
|
||||||
t("free_to_use_apps")
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
@@ -251,16 +257,16 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h4 className="mt-8 mb-2 font-semibold text-gray-900 ">{t("learn_more")}</h4>
|
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("learn_more")}</h4>
|
||||||
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
||||||
{docs && (
|
{docs && (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-sm font-normal text-black no-underline hover:underline"
|
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -270,9 +276,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -282,9 +288,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -295,9 +301,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -307,20 +313,20 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<hr className="my-8" />
|
<hr className="border-subtle my-8 border" />
|
||||||
<span className="leading-1 block text-xs text-gray-500">
|
<span className="leading-1 text-subtle block text-xs">
|
||||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||||
</span>
|
</span>
|
||||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,7 +360,7 @@ export default function App(props: {
|
|||||||
privacy?: string;
|
privacy?: string;
|
||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
images?: string[];
|
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ function CalendarSwitch(props: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!!props.destination && (
|
{!!props.destination && (
|
||||||
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
|
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
|
||||||
<FiArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
{t("adding_events_to")}
|
{t("adding_events_to")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -177,14 +177,14 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
trashIcon
|
trashIcon
|
||||||
onSuccess={props.onChanged}
|
onSuccess={props.onChanged}
|
||||||
buttonProps={{ className: "border border-gray-300" }}
|
buttonProps={{ className: "border border-default" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="border-t border-gray-200">
|
<div className="border-subtle border-t">
|
||||||
{!fromOnboarding && (
|
{!fromOnboarding && (
|
||||||
<>
|
<>
|
||||||
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||||
<ul className="space-y-2 px-5 py-4">
|
<ul className="space-y-2 px-5 py-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
@@ -218,7 +218,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
trashIcon
|
trashIcon
|
||||||
onSuccess={props.onChanged}
|
onSuccess={props.onChanged}
|
||||||
buttonProps={{ className: "border border-gray-300" }}
|
buttonProps={{ className: "border border-default" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||||
<>
|
<>
|
||||||
{heading && (
|
{heading && (
|
||||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||||
<ShellSubHeading
|
<ShellSubHeading
|
||||||
title={t("calendar")}
|
title={t("calendar")}
|
||||||
subtitle={t("installed_app_calendar_description")}
|
subtitle={t("installed_app_calendar_description")}
|
||||||
@@ -279,18 +279,18 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
||||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||||
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
|
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
||||||
<FiCalendar className="h-8 w-8" strokeWidth="1" />
|
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||||
<FiPlus
|
<Plus
|
||||||
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-6/12">
|
<div className="md:w-6/12">
|
||||||
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
|
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
|
||||||
<p className="text-sm font-normal">{t("set_calendar")}</p>
|
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-end md:w-6/12">
|
<div className="justify-end md:w-6/12">
|
||||||
<DestinationCalendarSelector
|
<DestinationCalendarSelector
|
||||||
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiCalendar}
|
Icon={Calendar}
|
||||||
headline={t("no_category_apps", {
|
headline={t("no_category_apps", {
|
||||||
category: t("calendar").toLowerCase(),
|
category: t("calendar").toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: `${selected.name} (${selected.externalId})` || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: `${cal.name} (${cal.externalId})` || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative flex items-center"
|
|
||||||
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
option: (defaultStyles, state) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
backgroundColor: state.isSelected
|
|
||||||
? state.isFocused
|
|
||||||
? "var(--brand-color)"
|
|
||||||
: "var(--brand-color)"
|
|
||||||
: state.isFocused
|
|
||||||
? "var(--brand-color-dark-mode)"
|
|
||||||
: "var(--brand-text-color)",
|
|
||||||
}),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -6,7 +6,7 @@ import React from "react";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmptyScreen } from "@calcom/ui";
|
import { EmptyScreen } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiAlertCircle}
|
Icon={AlertCircle}
|
||||||
headline={t("no_apps")}
|
headline={t("no_apps")}
|
||||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||||
|
|||||||
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
|
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
name: "calendar",
|
name: "calendar",
|
||||||
href: "/apps/installed/calendar",
|
href: "/apps/installed/calendar",
|
||||||
icon: FiCalendar,
|
icon: Calendar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conferencing",
|
name: "conferencing",
|
||||||
href: "/apps/installed/conferencing",
|
href: "/apps/installed/conferencing",
|
||||||
icon: FiVideo,
|
icon: Video,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payment",
|
name: "payment",
|
||||||
href: "/apps/installed/payment",
|
href: "/apps/installed/payment",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "automation",
|
name: "automation",
|
||||||
href: "/apps/installed/automation",
|
href: "/apps/installed/automation",
|
||||||
icon: FiShare2,
|
icon: Share2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "analytics",
|
name: "analytics",
|
||||||
href: "/apps/installed/analytics",
|
href: "/apps/installed/analytics",
|
||||||
icon: FiBarChart,
|
icon: BarChart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other",
|
name: "other",
|
||||||
href: "/apps/installed/other",
|
href: "/apps/installed/other",
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function TwoFactor({ center = true }) {
|
|||||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
|
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function AdminLayout({
|
|||||||
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
|
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
|
||||||
return (
|
return (
|
||||||
<SettingsLayout {...rest}>
|
<SettingsLayout {...rest}>
|
||||||
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
|
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button, SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
|
import { MoreHorizontal } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -20,8 +20,8 @@ export default SkeletonLoader;
|
|||||||
function SkeletonItem() {
|
function SkeletonItem() {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
<div className="flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
||||||
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
|
<div className="items-between flex w-full flex-col justify-center sm:px-6">
|
||||||
<SkeletonText className="my-1 h-4 w-32" />
|
<SkeletonText className="my-1 h-4 w-32" />
|
||||||
<SkeletonText className="my-1 h-2 w-24" />
|
<SkeletonText className="my-1 h-2 w-24" />
|
||||||
<SkeletonText className="my-1 h-2 w-40" />
|
<SkeletonText className="my-1 h-2 w-40" />
|
||||||
@@ -31,7 +31,7 @@ function SkeletonItem() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiMoreHorizontal}
|
StartIcon={MoreHorizontal}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@ export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
|
"border-subtle group flex w-full items-center justify-between rounded-sm border px-[10px] py-3",
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<div className="flex-grow truncate text-sm">
|
<div className="flex-grow truncate text-sm">
|
||||||
|
|||||||
@@ -4,58 +4,65 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
|||||||
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 { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiLink } from "@calcom/ui/components/icon";
|
import { Link } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
|
const excludeNullValues = (value: unknown) => !!value;
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
const renderLocations = locations.map((location, index) => {
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
{locations.map((location, index) => {
|
if (!eventLocationType) {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
// It's possible that the location app got uninstalled
|
||||||
if (!eventLocationType) {
|
return null;
|
||||||
// It's possible that the location app got uninstalled
|
}
|
||||||
return null;
|
if (eventLocationType.variable === "hostDefault") {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"attendee_in_person",
|
||||||
"in_person",
|
"in_person",
|
||||||
"attendee_phone_number",
|
"attendee_phone_number",
|
||||||
"link_meeting",
|
"link_meeting",
|
||||||
"organizer_phone_number",
|
"organizer_phone_number",
|
||||||
];
|
];
|
||||||
|
|
||||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||||
|
const translatedLocation = location.type.startsWith("integrations:")
|
||||||
|
? eventLocationType.label
|
||||||
|
: translateAbleKeys.includes(locationKey)
|
||||||
|
? t(locationKey)
|
||||||
|
: locationKey;
|
||||||
|
|
||||||
const translatedLocation = location.type.startsWith("integrations:")
|
return (
|
||||||
? eventLocationType.label
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
: translateAbleKeys.includes(locationKey)
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
? t(locationKey)
|
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
: locationKey;
|
) : (
|
||||||
|
<img
|
||||||
return (
|
src={eventLocationType.iconUrl}
|
||||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
className={classNames(
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={eventLocationType.iconUrl}
|
|
||||||
className={classNames(
|
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
|
||||||
)}
|
|
||||||
alt={`${eventLocationType.label} icon`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Tooltip content={translatedLocation}>
|
alt={`${eventLocationType.label} icon`}
|
||||||
<p className="line-clamp-1">{translatedLocation}</p>
|
/>
|
||||||
</Tooltip>
|
)}
|
||||||
</div>
|
<Tooltip content={translatedLocation}>
|
||||||
);
|
<p className="line-clamp-1">{translatedLocation}</p>
|
||||||
})}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
|
||||||
|
return filteredLocations.length ? (
|
||||||
|
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
|
||||||
|
{filteredLocations}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
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 { trpc } from "@calcom/trpc/react";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
|
|||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
ethSignature?: string;
|
ethSignature?: string;
|
||||||
|
duration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
|
duration,
|
||||||
}) => {
|
}) => {
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,16 +67,24 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
[isMobile]
|
[isMobile]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reserveSlot = (slot: Slot) => {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
slotUtcStartDate: slot.time,
|
||||||
|
eventTypeId,
|
||||||
|
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={slotPickerRef}>
|
<div ref={slotPickerRef}>
|
||||||
{!!date ? (
|
{!!date ? (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
||||||
<div className="mb-4 flex items-center text-left text-base">
|
<div className="mb-4 flex items-center text-left text-base">
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
<span className="text-emphasis font-semibold">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-bookinglight">
|
<span className="text-subtle">
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +106,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slots.map((slot) => {
|
slots.map((slot) => {
|
||||||
type BookingURL = {
|
type BookingURL = {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
query: Record<string, string | number | string[] | undefined>;
|
query: Record<string, string | number | string[] | undefined | TimeFormat>;
|
||||||
};
|
};
|
||||||
const bookingUrl: BookingURL = {
|
const bookingUrl: BookingURL = {
|
||||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
@@ -103,6 +115,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||||
type: eventTypeId,
|
type: eventTypeId,
|
||||||
slug: eventTypeSlug,
|
slug: eventTypeSlug,
|
||||||
|
timeFormat,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
@@ -131,7 +144,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
|
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
@@ -146,10 +159,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
href={bookingUrl}
|
href={bookingUrl}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
|
onClick={() => reserveSlot(slot)}
|
||||||
data-testid="time">
|
data-testid="time">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && (
|
{!!seatsPerTimeSlot && (
|
||||||
@@ -173,7 +186,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
|
|
||||||
{!isLoading && !slots.length && (
|
{!isLoading && !slots.length && (
|
||||||
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
||||||
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
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 { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
|
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -83,32 +83,27 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
|
||||||
{profile.name}
|
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
||||||
</h2>
|
|
||||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
|
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
|
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
|
||||||
{eventType?.description && (
|
{eventType?.description && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "text-default text-sm font-medium"
|
||||||
)}>
|
)}>
|
||||||
|
{/* TODO: Fix colors when token is introdcued to DS */}
|
||||||
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div
|
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
|
||||||
className={classNames(
|
|
||||||
"items-top flex",
|
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
|
||||||
)}>
|
|
||||||
<div>
|
<div>
|
||||||
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
</div>
|
</div>
|
||||||
{requiresConfirmationText}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +114,12 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-nowrap text-sm font-medium",
|
"flex flex-nowrap text-sm font-medium",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-gray-600",
|
isBookingPage && "text-default",
|
||||||
!eventType.metadata?.multipleDuration && "items-center"
|
!eventType.metadata?.multipleDuration && "items-center"
|
||||||
)}>
|
)}>
|
||||||
<FiClock
|
<Clock
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
isBookingPage && "mt-[2px]"
|
isBookingPage && "mt-[2px]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -135,12 +130,11 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="gray"
|
variant="gray"
|
||||||
size="lg"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
duration === dur.toString()
|
duration === dur.toString()
|
||||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
? "bg-emphasis border-emphasis text-emphasis "
|
||||||
: "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",
|
: "bg-subtle text-default border-transparent ",
|
||||||
"cursor-pointer"
|
"cursor-pointer border"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDuration(dur);
|
setDuration(dur);
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
|
import type { TFunction } from "next-i18next";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { FiCreditCard } from "@calcom/ui/components/icon";
|
import { CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
const BookingDescriptionPayment = (props: {
|
||||||
|
eventType: Parameters<typeof getPaymentAppData>[0];
|
||||||
|
t: TFunction;
|
||||||
|
}) => {
|
||||||
const paymentAppData = getPaymentAppData(props.eventType);
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>
|
||||||
value={paymentAppData.price / 100.0}
|
{props.t("no_show_fee_amount", {
|
||||||
style="currency"
|
amount: paymentAppData.price / 100.0,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
/>
|
})}
|
||||||
</IntlProvider>
|
</>
|
||||||
|
) : (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||||
|
import "@calcom/dayjs/locales";
|
||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
@@ -26,10 +28,11 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
|
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
|
|
||||||
@@ -46,12 +49,17 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
// Get user so we can determine 12/24 hour format preferences
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
const query = useMeQuery();
|
const query = useMeQuery();
|
||||||
const user = query.data;
|
const user = query.data;
|
||||||
const { t } = useLocale();
|
const {
|
||||||
|
t,
|
||||||
|
i18n: { language },
|
||||||
|
} = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
|
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
|
const cardCharged = booking?.payment[0]?.success;
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data?.status === BookingStatus.REJECTED) {
|
if (data?.status === BookingStatus.REJECTED) {
|
||||||
@@ -108,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: FiSlash,
|
icon: Slash,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
{
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
id: "confirm",
|
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
||||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
? [
|
||||||
onClick: () => {
|
{
|
||||||
bookingConfirm(true);
|
id: "confirm",
|
||||||
},
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||||
icon: FiCheck,
|
onClick: () => {
|
||||||
disabled: mutation.isLoading,
|
bookingConfirm(true);
|
||||||
color: "primary",
|
},
|
||||||
},
|
icon: Check,
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const showRecordingActions: ActionType[] = [
|
const showRecordingActions: ActionType[] = [
|
||||||
@@ -144,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||||
`,
|
`,
|
||||||
icon: FiX,
|
icon: X,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "edit_booking",
|
id: "edit_booking",
|
||||||
@@ -152,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "reschedule",
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
label: t("reschedule_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}${
|
href: `/reschedule/${booking.uid}${
|
||||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||||
@@ -160,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: FiSend,
|
icon: Send,
|
||||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||||
label: t("send_reschedule_request"),
|
label: t("send_reschedule_request"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -173,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenLocationDialog(true);
|
setIsOpenLocationDialog(true);
|
||||||
},
|
},
|
||||||
icon: FiMapPin,
|
icon: MapPin,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const chargeCardActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: "charge_card",
|
||||||
|
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||||
|
disabled: cardCharged,
|
||||||
|
onClick: () => {
|
||||||
|
setChargeCardDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
icon: CreditCard,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (isTabRecurring && isRecurring) {
|
if (isTabRecurring && isRecurring) {
|
||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
@@ -189,13 +213,15 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
|
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||||
{t("reschedule_request_sent")}
|
{t("reschedule_request_sent")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
const startTime = dayjs(booking.startTime)
|
||||||
|
.locale(language)
|
||||||
|
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||||
@@ -230,10 +256,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const title = booking.title;
|
||||||
|
// To be used after we run query on legacy bookings
|
||||||
|
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||||
|
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons =
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
const title = booking.title;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -247,6 +277,15 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isOpenDialog={isOpenSetLocationDialog}
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
setShowLocationModal={setIsOpenLocationDialog}
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
/>
|
/>
|
||||||
|
{booking.paid && (
|
||||||
|
<ChargeCardDialog
|
||||||
|
isOpenDialog={chargeCardDialogIsOpen}
|
||||||
|
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||||
|
bookingId={booking.id}
|
||||||
|
paymentAmount={booking?.payment[0].amount}
|
||||||
|
paymentCurrency={booking?.payment[0].currency}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showRecordingsButtons && (
|
{showRecordingsButtons && (
|
||||||
<ViewRecordingsDialog
|
<ViewRecordingsDialog
|
||||||
booking={booking}
|
booking={booking}
|
||||||
@@ -264,7 +303,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
{t("rejection_reason")}
|
{t("rejection_reason")}
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<span className="text-subtle font-normal"> (Optional)</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
value={rejectionReason}
|
value={rejectionReason}
|
||||||
@@ -285,13 +324,13 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
|
<tr className="hover:bg-muted group flex flex-col 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}>
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-subtle text-sm">
|
||||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
<MeetingTimeInTimezones
|
<MeetingTimeInTimezones
|
||||||
@@ -314,11 +353,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{booking.paid && (
|
{booking.paid && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{t("paid")}
|
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
<div className="text-muted mt-2 text-sm">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -328,8 +367,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||||
<div className="flex w-full items-center justify-between sm:hidden">
|
<div className="flex w-full items-center justify-between sm:hidden">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||||
<div className="pr-2 text-sm text-gray-500">
|
<div className="text-subtle pr-2 text-sm">
|
||||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
<MeetingTimeInTimezones
|
<MeetingTimeInTimezones
|
||||||
@@ -358,7 +397,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="text-sm text-gray-400 sm:hidden">
|
<div className="text-muted text-sm sm:hidden">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -368,21 +407,21 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<div
|
<div
|
||||||
title={title}
|
title={title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{title}
|
{title}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
|
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
||||||
{t("pending_payment")}
|
{t("pending_payment")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -406,7 +445,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<>
|
<>
|
||||||
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||||
{isConfirmed && <TableActions actions={bookedActions} />}
|
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||||
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
@@ -416,6 +455,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
|
||||||
|
<div className="ml-2">
|
||||||
|
<TableActions actions={chargeCardActions} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -431,7 +475,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
// Get user so we can determine 12/24 hour format preferences
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
const query = useMeQuery();
|
const query = useMeQuery();
|
||||||
const user = query.data;
|
const user = query.data;
|
||||||
const { t } = useLocale();
|
const {
|
||||||
|
t,
|
||||||
|
i18n: { language },
|
||||||
|
} = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const recurringCount = recurringDates.filter((date) => {
|
const recurringCount = recurringDates.filter((date) => {
|
||||||
return (
|
return (
|
||||||
@@ -461,14 +508,14 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
||||||
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
||||||
{" - "}
|
{" - "}
|
||||||
{dayjs(aDate).format("D MMMM YYYY")}
|
{dayjs(aDate).locale(language).format("D MMMM YYYY")}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}>
|
})}>
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="text-default">
|
||||||
<FiRefreshCcw
|
<RefreshCcw
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 pl-5 text-xs">
|
<p className="mt-1 pl-5 text-xs">
|
||||||
{booking.status === BookingStatus.ACCEPTED
|
{booking.status === BookingStatus.ACCEPTED
|
||||||
@@ -541,13 +588,13 @@ const DisplayAttendees = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-emphasis text-sm">
|
||||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
||||||
<Attendee {...attendees[0]} />
|
<Attendee {...attendees[0]} />
|
||||||
{attendees.length > 1 && (
|
{attendees.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<div className="inline-block text-sm text-gray-900"> {t("and")} </div>
|
<div className="text-emphasis inline-block text-sm"> {t("and")} </div>
|
||||||
{attendees.length > 2 ? (
|
{attendees.length > 2 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={attendees.slice(1).map((attendee) => (
|
content={attendees.slice(1).map((attendee) => (
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useRouter } from "next/router";
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
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 { Button, TextArea } from "@calcom/ui";
|
import { Button, TextArea } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||||
useTheme(props.theme);
|
|
||||||
|
|
||||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
@@ -47,11 +45,11 @@ export default function CancelBooking(props: Props) {
|
|||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<FiX className="h-6 w-6 text-red-600" />
|
<X className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{error}
|
{error}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,13 +57,13 @@ export default function CancelBooking(props: Props) {
|
|||||||
)}
|
)}
|
||||||
{!error && (
|
{!error && (
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={cancelBookingRef}
|
ref={cancelBookingRef}
|
||||||
placeholder={t("cancellation_reason_placeholder")}
|
placeholder={t("cancellation_reason_placeholder")}
|
||||||
value={cancellationReason}
|
value={cancellationReason}
|
||||||
onChange={(e) => setCancellationReason(e.target.value)}
|
onChange={(e) => setCancellationReason(e.target.value)}
|
||||||
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
|
className="mt-2 mb-4 w-full "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||||
@@ -77,7 +75,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex justify-center"
|
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -86,7 +83,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
|
|
||||||
const res = await fetch("/api/cancel", {
|
const res = await fetch("/api/cancel", {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: booking?.id,
|
uid: booking?.uid,
|
||||||
cancellationReason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
allRemainingBookings,
|
allRemainingBookings,
|
||||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
|
|||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime:
|
startTime: dayjs().startOf("day"),
|
||||||
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,
|
duration,
|
||||||
@@ -167,7 +164,7 @@ export const SlotPicker = ({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
||||||
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
||||||
)}
|
)}
|
||||||
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
@@ -197,6 +194,7 @@ export const SlotPicker = ({
|
|||||||
bookingAttendees={bookingAttendees}
|
bookingAttendees={bookingAttendees}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
ethSignature={ethSignature}
|
ethSignature={ethSignature}
|
||||||
|
duration={parseInt(duration)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import type { ITimezoneOption } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
import { TimezoneSelect } from "@calcom/ui";
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,8 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const query = useMeQuery();
|
|
||||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -28,59 +23,17 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
|
||||||
padding: "10px 12px !important",
|
|
||||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
|
||||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
control: () => ({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "transparent !important",
|
|
||||||
minWidth: "5rem",
|
|
||||||
height: "24px",
|
|
||||||
minHeight: "24px !important",
|
|
||||||
boxShadow: "none !important",
|
|
||||||
}),
|
|
||||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#a5a5a5 !important",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
menu: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
minWidth: "20rem",
|
|
||||||
margin: "8px 0 0 -20px",
|
|
||||||
}),
|
|
||||||
valueContainer: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
padding: "0 0 0 6px",
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
marginLeft: 4,
|
|
||||||
padding: 0,
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return !!selectedTimeZone ? (
|
return !!selectedTimeZone ? (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
classNames={{
|
||||||
|
singleValue: () => "text-default",
|
||||||
|
dropdownIndicator: () => "text-default",
|
||||||
|
menu: () => "!w-64 max-w-[90vw]",
|
||||||
|
}}
|
||||||
|
variant="minimal"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
className="flex h-6 text-sm font-medium"
|
|
||||||
styles={customStyles}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FiGlobe } from "@calcom/ui/components/icon";
|
import { Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,18 +14,17 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
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 notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
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";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
|
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -58,12 +57,24 @@ const dateQuerySchema = z.object({
|
|||||||
|
|
||||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
|
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
|
||||||
|
const brandTheme = useGetBrandingColours({
|
||||||
|
lightVal: brandColor,
|
||||||
|
darkVal: darkBrandColor,
|
||||||
|
});
|
||||||
|
useCalcomTheme(brandTheme);
|
||||||
|
};
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const query = dateQuerySchema.parse(router.query);
|
const query = dateQuerySchema.parse(router.query);
|
||||||
const { rescheduleUid } = query;
|
const { rescheduleUid } = query;
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
|
useBrandColors({
|
||||||
|
brandColor: profile.brandColor,
|
||||||
|
darkBrandColor: profile.darkBrandColor,
|
||||||
|
});
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
@@ -93,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
|
/*
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
@@ -102,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]); */
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
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) : [];
|
||||||
@@ -112,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
const paymentAmount = () => {
|
||||||
|
return;
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>;
|
||||||
|
};
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
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.
|
||||||
@@ -154,32 +176,30 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
nofollow: eventType.hidden,
|
nofollow: eventType.hidden,
|
||||||
noindex: eventType.hidden,
|
noindex: eventType.hidden,
|
||||||
}}
|
}}
|
||||||
|
isBrandingHidden={restProps.isBrandingHidden}
|
||||||
/>
|
/>
|
||||||
<BookingPageTagManager eventType={eventType} />
|
<BookingPageTagManager eventType={eventType} />
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
|
||||||
<div>
|
<div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-col md:mx-4",
|
"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("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={availabilityDatePickerEmbedStyles}
|
style={availabilityDatePickerEmbedStyles}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isBackgroundTransparent
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
|
||||||
? ""
|
"border-booker md:border-booker-width md:rounded-md",
|
||||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
|
||||||
"border-bookinglightest md:rounded-md md:border",
|
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="md:flex">
|
<div className="md:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
" border-subtle flex flex-col p-5 sm:border-r",
|
||||||
"min-w-full md:w-[280px] md:min-w-[280px]",
|
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||||
)}>
|
)}>
|
||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
@@ -187,10 +207,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-nowrap items-center text-sm font-medium",
|
"flex flex-nowrap items-center text-sm font-medium",
|
||||||
"dark:text-darkgray-600 text-gray-600",
|
" text-default",
|
||||||
"ltr:mr-[10px] rtl:ml-[10px]"
|
"ltr:mr-[10px] rtl:ml-[10px]"
|
||||||
)}>
|
)}>
|
||||||
<FiUser
|
<User
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
"mt-[2px]"
|
"mt-[2px]"
|
||||||
@@ -201,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="flex items-start text-sm font-medium">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
<input
|
|
||||||
type="number"
|
<NumberInput
|
||||||
|
defaultValue={eventType.recurringEvent.count}
|
||||||
min="1"
|
min="1"
|
||||||
max={eventType.recurringEvent.count}
|
max={eventType.recurringEvent.count}
|
||||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
isFullWidth={false}
|
||||||
defaultValue={eventType.recurringEvent.count}
|
className="me-2 inline w-16"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="inline">
|
<p className="inline">
|
||||||
{t("occurrence", {
|
{t("occurrence", {
|
||||||
count: recurringEventCount,
|
count: recurringEventCount,
|
||||||
@@ -226,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{paymentAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>
|
||||||
value={paymentAppData.price / 100.0}
|
{t("no_show_fee_amount", {
|
||||||
style="currency"
|
amount: paymentAppData.price / 100.0,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
/>
|
})}
|
||||||
</IntlProvider>
|
</>
|
||||||
|
) : (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{timezoneDropdown}
|
{timezoneDropdown}
|
||||||
@@ -242,12 +273,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
{/* 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-default"
|
||||||
data-testid="former_time_p_desktop">
|
data-testid="former_time_p_desktop">
|
||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
<p className="text-subtle line-through ">
|
||||||
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import { useSession } from "next-auth/react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { useForm, useFormContext } from "react-hook-form";
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, locationKeyToString } 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";
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -30,25 +28,28 @@ import {
|
|||||||
import getBookingResponsesSchema, {
|
import getBookingResponsesSchema, {
|
||||||
getBookingResponsesPartialSchema,
|
getBookingResponsesPartialSchema,
|
||||||
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
|
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||||
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
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 { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
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 { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
||||||
|
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
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 { parseRecurringDates, parseDate } from "@lib/parseDate";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import Gates from "@components/Gates";
|
import Gates from "@components/Gates";
|
||||||
@@ -58,11 +59,21 @@ import type { BookPageProps } from "../../../pages/[user]/book";
|
|||||||
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||||
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
|
|
||||||
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
|
|
||||||
/** These are like 40kb that not every user needs */
|
/** These are like 40kb that not every user needs */
|
||||||
const BookingDescriptionPayment = dynamic(
|
const BookingDescriptionPayment = dynamic(
|
||||||
() => import("@components/booking/BookingDescriptionPayment")
|
() => import("@components/booking/BookingDescriptionPayment")
|
||||||
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
|
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
|
||||||
|
|
||||||
|
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
|
||||||
|
const brandTheme = useGetBrandingColours({
|
||||||
|
lightVal: brandColor,
|
||||||
|
darkVal: darkBrandColor,
|
||||||
|
});
|
||||||
|
useCalcomTheme(brandTheme);
|
||||||
|
};
|
||||||
|
|
||||||
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
||||||
const BookingFields = ({
|
const BookingFields = ({
|
||||||
fields,
|
fields,
|
||||||
@@ -78,6 +89,7 @@ const BookingFields = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { watch, setValue } = useFormContext();
|
const { watch, setValue } = useFormContext();
|
||||||
const locationResponse = watch("responses.location");
|
const locationResponse = watch("responses.location");
|
||||||
|
const currentView = rescheduleUid ? "reschedule" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
||||||
@@ -87,12 +99,16 @@ const BookingFields = ({
|
|||||||
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
||||||
let readOnly =
|
let readOnly =
|
||||||
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
||||||
|
|
||||||
let noLabel = false;
|
let noLabel = false;
|
||||||
let hidden = !!field.hidden;
|
let hidden = !!field.hidden;
|
||||||
|
const fieldViews = field.views;
|
||||||
|
|
||||||
|
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.name === SystemField.Enum.rescheduleReason) {
|
if (field.name === SystemField.Enum.rescheduleReason) {
|
||||||
if (!rescheduleUid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
}
|
}
|
||||||
@@ -129,26 +145,14 @@ const BookingFields = ({
|
|||||||
}
|
}
|
||||||
const optionsInputs = field.optionsInputs;
|
const optionsInputs = field.optionsInputs;
|
||||||
|
|
||||||
const options = locations.map((location) => {
|
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
|
||||||
const eventLocation = getEventLocationType(location.type);
|
const options = getLocationOptionsForSelect(locations, t);
|
||||||
const locationString = locationKeyToString(location);
|
options.forEach((option) => {
|
||||||
|
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
|
||||||
if (typeof locationString !== "string" || !eventLocation) {
|
|
||||||
// It's possible that location app got uninstalled
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const type = eventLocation.type;
|
|
||||||
const optionInput = optionsInputs[type as keyof typeof optionsInputs];
|
|
||||||
if (optionInput) {
|
if (optionInput) {
|
||||||
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || "");
|
optionInput.placeholder = option.inputPlaceholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
label: t(locationString),
|
|
||||||
value: type,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
field.options = options.filter(
|
field.options = options.filter(
|
||||||
(location): location is NonNullable<(typeof options)[number]> => !!location
|
(location): location is NonNullable<(typeof options)[number]> => !!location
|
||||||
);
|
);
|
||||||
@@ -180,9 +184,26 @@ const BookingFields = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const routerQuerySchema = z
|
||||||
|
.object({
|
||||||
|
timeFormat: z.nativeEnum(TimeFormat),
|
||||||
|
rescheduleUid: z.string().optional(),
|
||||||
|
date: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((date) => {
|
||||||
|
if (date === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const BookingPage = ({
|
const BookingPage = ({
|
||||||
eventType,
|
eventType,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking,
|
||||||
profile,
|
profile,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
@@ -190,8 +211,11 @@ const BookingPage = ({
|
|||||||
hashedLink,
|
hashedLink,
|
||||||
...restProps
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
|
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { duration: queryDuration } = useRouterQuery("duration");
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
|
const { date: queryDate } = useRouterQuery("date");
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
@@ -207,6 +231,15 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
const reserveSlot = () => {
|
||||||
|
if (queryDuration) {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||||
|
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (
|
if (
|
||||||
@@ -218,28 +251,20 @@ const BookingPage = ({
|
|||||||
duration = Number(queryDuration);
|
duration = Number(queryDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
|
|
||||||
// At initial render, we use the default time format which is 12H
|
|
||||||
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
|
|
||||||
const parseDateFunc = useCallback(
|
|
||||||
(date: string | null | Dayjs) => {
|
|
||||||
return parseDate(date, i18n, withDefaultTimeFormat);
|
|
||||||
},
|
|
||||||
[withDefaultTimeFormat]
|
|
||||||
);
|
|
||||||
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWithDefaultTimeFormat(false);
|
/* if (top !== window) {
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (top !== window) {
|
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
telemetryEventTypes.embedView,
|
telemetryEventTypes.embedView,
|
||||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
|
reserveSlot();
|
||||||
|
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
removeSelectedSlotMarkMutation.mutate();
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -259,15 +284,19 @@ const BookingPage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push({
|
const query = {
|
||||||
pathname: `/booking/${uid}`,
|
isSuccessBookingPage: true,
|
||||||
query: {
|
email: bookingForm.getValues("responses.email"),
|
||||||
isSuccessBookingPage: true,
|
eventTypeSlug: eventType.slug,
|
||||||
email: bookingForm.getValues("responses.email"),
|
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
||||||
eventTypeSlug: eventType.slug,
|
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||||
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
};
|
||||||
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
|
||||||
},
|
return bookingSuccessRedirect({
|
||||||
|
router,
|
||||||
|
successRedirectUrl: eventType.successRedirectUrl,
|
||||||
|
query,
|
||||||
|
bookingUid: uid,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -275,24 +304,37 @@ const BookingPage = ({
|
|||||||
const recurringMutation = useMutation(createRecurringBooking, {
|
const recurringMutation = useMutation(createRecurringBooking, {
|
||||||
onSuccess: async (responseData = []) => {
|
onSuccess: async (responseData = []) => {
|
||||||
const { uid } = responseData[0] || {};
|
const { uid } = responseData[0] || {};
|
||||||
|
const query = {
|
||||||
return router.push({
|
isSuccessBookingPage: true,
|
||||||
pathname: `/booking/${uid}`,
|
allRemainingBookings: true,
|
||||||
query: {
|
email: bookingForm.getValues("responses.email"),
|
||||||
allRemainingBookings: true,
|
eventTypeSlug: eventType.slug,
|
||||||
email: bookingForm.getValues("responses.email"),
|
formerTime: booking?.startTime.toString(),
|
||||||
eventTypeSlug: eventType.slug,
|
};
|
||||||
formerTime: booking?.startTime.toString(),
|
return bookingSuccessRedirect({
|
||||||
},
|
router,
|
||||||
|
successRedirectUrl: eventType.successRedirectUrl,
|
||||||
|
query,
|
||||||
|
bookingUid: uid,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rescheduleUid = router.query.rescheduleUid as string;
|
const {
|
||||||
|
data: { timeFormat, rescheduleUid, date },
|
||||||
|
} = useTypedQuery(routerQuerySchema);
|
||||||
|
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const date = asStringOrNull(router.query.date);
|
useBrandColors({
|
||||||
|
brandColor: profile.brandColor,
|
||||||
|
darkBrandColor: profile.darkBrandColor,
|
||||||
|
});
|
||||||
|
|
||||||
const querySchema = getBookingResponsesPartialSchema({
|
const querySchema = getBookingResponsesPartialSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: {
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
|
},
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedQuery = querySchema.parse({
|
const parsedQuery = querySchema.parse({
|
||||||
@@ -372,7 +414,8 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
responses: getBookingResponsesSchema({
|
responses: getBookingResponsesSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -390,26 +433,17 @@ const BookingPage = ({
|
|||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [],
|
let recurringStrings: string[] = [],
|
||||||
recurringDates: Date[] = [];
|
recurringDates: Date[] = [];
|
||||||
const parseRecurringDatesFunc = useCallback(
|
|
||||||
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
|
|
||||||
return parseRecurringDates(
|
|
||||||
{
|
|
||||||
startDate: date,
|
|
||||||
timeZone: timeZone(),
|
|
||||||
recurringEvent: recurringEvent,
|
|
||||||
recurringCount: recurringCount,
|
|
||||||
withDefaultTimeFormat: withDefaultTimeFormat,
|
|
||||||
},
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
|
|
||||||
);
|
|
||||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||||
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
|
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||||
date,
|
{
|
||||||
eventType.recurringEvent,
|
startDate: date,
|
||||||
parseInt(recurringEventCount.toString())
|
timeZone: timeZone(),
|
||||||
|
recurringEvent: eventType.recurringEvent,
|
||||||
|
recurringCount: parseInt(recurringEventCount.toString()),
|
||||||
|
selectedTimeFormat: timeFormat,
|
||||||
|
},
|
||||||
|
i18n
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,8 +471,8 @@ const BookingPage = ({
|
|||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||||
...bookingValues,
|
...bookingValues,
|
||||||
start: dayjs(recurringDate).format(),
|
start: dayjs(recurringDate).utc().format(),
|
||||||
end: dayjs(recurringDate).add(duration, "minute").format(),
|
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
recurringEventId,
|
recurringEventId,
|
||||||
@@ -457,8 +491,8 @@ const BookingPage = ({
|
|||||||
} else {
|
} else {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...bookingValues,
|
...bookingValues,
|
||||||
start: dayjs(date).tz(timeZone()).format(),
|
start: dayjs(date).utc().format(),
|
||||||
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
|
end: dayjs(date).utc().add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
timeZone: timeZone(),
|
timeZone: timeZone(),
|
||||||
@@ -504,7 +538,6 @@ const BookingPage = ({
|
|||||||
<link rel="icon" href="/favico.ico" />
|
<link rel="icon" href="/favico.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<BookingPageTagManager eventType={eventType} />
|
<BookingPageTagManager eventType={eventType} />
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
@@ -514,17 +547,17 @@ const BookingPage = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main",
|
"main",
|
||||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
|
||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"border-booker sm:border-booker-width rounded-md"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<BookingDescriptionPayment eventType={eventType} />
|
<BookingDescriptionPayment eventType={eventType} t={t} />
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
<div className="dark:text-inverted text-default items-start text-sm font-medium">
|
||||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<p className="-ml-2 inline-block items-center px-2">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{getEveryFreqFor({
|
{getEveryFreqFor({
|
||||||
t,
|
t,
|
||||||
@@ -535,11 +568,11 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
`${parseDateFunc(date)}`}
|
`${parseDate(date, i18n, timeFormat)}`}
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
!rescheduleUid &&
|
!rescheduleUid &&
|
||||||
eventType.recurringEvent?.freq &&
|
eventType.recurringEvent?.freq &&
|
||||||
@@ -552,7 +585,7 @@ const BookingPage = ({
|
|||||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
<p key={key}>{timeFormatted}</p>
|
<p key={key}>{timeFormatted}</p>
|
||||||
))}>
|
))}>
|
||||||
<p className="dark:text-darkgray-600 text-sm">
|
<p className=" text-sm">
|
||||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
</p>
|
</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -566,34 +599,38 @@ const BookingPage = ({
|
|||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="line-through ">
|
<p className="line-through ">
|
||||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDateFunc(dayjs(booking.startTime))}
|
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiUser
|
<User
|
||||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={`${
|
className={`${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
} mb-2 font-medium`}>
|
} mb-2 font-medium`}>
|
||||||
{booking
|
{currentSlotBooking
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
</p>
|
</p>
|
||||||
@@ -631,13 +668,14 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
{(mutation.isError || recurringMutation.isError) && (
|
{mutation.isError || recurringMutation.isError ? (
|
||||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</Gates>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -653,9 +691,9 @@ function ErrorMessage({ error }: { error: unknown }) {
|
|||||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ltr:ml-3 rtl:mr-3">
|
<div className="ms-3">
|
||||||
<p className="text-sm text-blue-700">
|
<p className="text-sm text-blue-700">
|
||||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||||
{error instanceof HttpError || error instanceof Error ? (
|
{error instanceof HttpError || error instanceof Error ? (
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { IntlProvider, FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
showToast,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
interface IRescheduleDialog {
|
||||||
|
isOpenDialog: boolean;
|
||||||
|
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
|
bookingId: number;
|
||||||
|
paymentAmount: number;
|
||||||
|
paymentCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
|
||||||
|
const [chargeError, setChargeError] = useState(false);
|
||||||
|
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.viewer.bookings.invalidate();
|
||||||
|
setIsOpenDialog(false);
|
||||||
|
showToast("Charge successful", "success");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setChargeError(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<div className="flex flex-row space-x-3">
|
||||||
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
|
<CreditCard className="m-auto h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="pt-1">
|
||||||
|
<DialogHeader title={t("charge_card")} />
|
||||||
|
<Trans i18nKey="charge_card_dialog_body">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
You are about to charge the attendee{" "}
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={props.paymentAmount / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={props.paymentCurrency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
. Are you sure you want to continue?
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
|
|
||||||
|
{chargeError && (
|
||||||
|
<div className="mt-4 flex text-red-500">
|
||||||
|
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||||
|
<p className="text-sm">{t("error_charging_card")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose />
|
||||||
|
<Button
|
||||||
|
data-testid="send_request"
|
||||||
|
disabled={chargeCardMutation.isLoading || chargeError}
|
||||||
|
onClick={() =>
|
||||||
|
chargeCardMutation.mutate({
|
||||||
|
bookingId,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Trans i18nKey="charge_card_confirm">
|
||||||
|
Charge attendee{" "}
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={props.paymentAmount / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={props.paymentCurrency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,8 +17,9 @@ import {
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Input } from "@calcom/ui";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { MapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ interface ISetLocationDialog {
|
|||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: LocationOption;
|
selection?: LocationOption;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
|
isTeamEvent?: boolean;
|
||||||
defaultValues?: LocationObject[];
|
defaultValues?: LocationObject[];
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -52,7 +54,7 @@ const LocationInput = (props: {
|
|||||||
const { control } = useFormContext() as typeof locationFormMethods;
|
const { control } = useFormContext() as typeof locationFormMethods;
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
if (eventLocationType?.organizerInputType === "text") {
|
||||||
return (
|
return (
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||||
);
|
);
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||||
return (
|
return (
|
||||||
@@ -73,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
saveLocation,
|
saveLocation,
|
||||||
selection,
|
selection,
|
||||||
booking,
|
booking,
|
||||||
|
isTeamEvent,
|
||||||
setShowLocationModal,
|
setShowLocationModal,
|
||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -161,7 +164,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="locationInput" className="text-default block text-sm font-medium">
|
||||||
{t(eventLocationType.messageForOrganizer || "")}
|
{t(eventLocationType.messageForOrganizer || "")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -171,7 +174,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
id="locationInput"
|
id="locationInput"
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
defaultValue={
|
defaultValue={
|
||||||
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
||||||
@@ -180,7 +183,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
errors={locationFormMethods.formState.errors}
|
errors={locationFormMethods.formState.errors}
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
className="mt-1 text-sm text-red-500"
|
className="text-error mt-1 text-sm"
|
||||||
as="p"
|
as="p"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +209,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
return <p className="text-default text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -214,16 +217,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-subtle 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">
|
||||||
<FiMapPin className="text-primary-600 h-6 w-6" />
|
<MapPin className="text-emphasis h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mt-3 text-center sm:mt-0 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-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{t("edit_location")}
|
{t("edit_location")}
|
||||||
</h3>
|
</h3>
|
||||||
{!booking && (
|
{!booking && (
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-default text-sm">
|
||||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
||||||
Can't find the right video app? Visit our
|
Can't find the right video app? Visit our
|
||||||
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
||||||
@@ -238,8 +241,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
{booking && (
|
{booking && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
<p className="text-emphasis mb-2 ml-1 text-sm">
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
{getHumanReadableLocationValue(booking.location, t)}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -290,7 +293,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
query={locationsQuery}
|
query={locationsQuery}
|
||||||
success={({ data }) => {
|
success={({ data }) => {
|
||||||
if (!data.length) return null;
|
if (!data.length) return null;
|
||||||
const locationOptions = [...data];
|
const locationOptions = [...data].filter((option) => {
|
||||||
|
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
if (booking) {
|
if (booking) {
|
||||||
locationOptions.map((location) =>
|
locationOptions.map((location) =>
|
||||||
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiClock } from "@calcom/ui/components/icon";
|
import { Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -44,14 +44,14 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
<FiClock className="m-auto h-6 w-6" />
|
<Clock className="m-auto h-6 w-6" />
|
||||||
</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="text-subtle text-sm">{t("reschedule_modal_description")}</p>
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
|
||||||
{t("reason_for_reschedule_request")}
|
{t("reason_for_reschedule_request")}
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<span className="text-subtle font-normal"> (Optional)</span>
|
||||||
</p>
|
</p>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="reschedule_reason"
|
data-testid="reschedule_reason"
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
|
<div className="bg-default overflow-hidden shadow sm:rounded-lg">
|
||||||
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
|
<div className="border-subtle border-t px-4 py-5 sm:p-0">
|
||||||
<dl className="sm:divide-y sm:divide-gray-200">
|
<dl className="sm:divide-subtle sm:divide-y">
|
||||||
{debugMap.map(([key, value]) => {
|
{debugMap.map(([key, value]) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
||||||
<dt className="text-sm font-bold text-black">{key}</dt>
|
<dt className="text-emphasis text-sm font-bold">{key}</dt>
|
||||||
<dd className="mt-1 text-sm text-black sm:col-span-2 sm:mt-0">{value}</dd>
|
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,11 +53,11 @@ export const ErrorPage: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-screen bg-white px-4">
|
<div className="bg-default min-h-screen px-4">
|
||||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{statusCode}</p>
|
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
|
||||||
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
|
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
|
||||||
{message}
|
{message}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
|
||||||
import type { OptionProps, SingleValueProps } from "react-select";
|
|
||||||
import { components } from "react-select";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
|
||||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
|
||||||
|
|
||||||
type AvailabilityOption = {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
isDefault: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|
||||||
const { label, isDefault } = props.data;
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<components.Option {...props}>
|
|
||||||
<span>{label}</span>
|
|
||||||
{isDefault && (
|
|
||||||
<Badge variant="blue" className="ml-2">
|
|
||||||
{t("default")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</components.Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|
||||||
const { label, isDefault } = props.data;
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<components.SingleValue {...props}>
|
|
||||||
<span>{label}</span>
|
|
||||||
{isDefault && (
|
|
||||||
<Badge variant="blue" className="ml-2">
|
|
||||||
{t("default")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</components.SingleValue>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AvailabilitySelect = ({
|
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
onBlur: () => void;
|
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
|
||||||
}) => {
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <SelectSkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedules = data?.schedules || [];
|
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
|
||||||
value: schedule.id,
|
|
||||||
label: schedule.name,
|
|
||||||
isDefault: schedule.isDefault,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const value = options.find((option) =>
|
|
||||||
props.value
|
|
||||||
? option.value === props.value
|
|
||||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder={t("select")}
|
|
||||||
options={options}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={props.onChange}
|
|
||||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
|
||||||
value={value}
|
|
||||||
components={{ Option, SingleValue }}
|
|
||||||
isMulti={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const format = (date: Date, hour12: boolean) =>
|
|
||||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
|
|
||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
|
||||||
);
|
|
||||||
|
|
||||||
const EventTypeScheduleDetails = () => {
|
|
||||||
const { data: loggedInUser } = useMeQuery();
|
|
||||||
const timeFormat = loggedInUser?.timeFormat;
|
|
||||||
const { t, i18n } = useLocale();
|
|
||||||
const { watch } = useFormContext<FormValues>();
|
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
|
||||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
|
||||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
|
||||||
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 rounded border px-6 pb-4">
|
|
||||||
<ol className="table border-collapse text-sm">
|
|
||||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
|
||||||
const isAvailable = !!filterDays(index).length;
|
|
||||||
return (
|
|
||||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"w-20 font-medium sm:w-32",
|
|
||||||
!isAvailable && "text-gray-500 opacity-50"
|
|
||||||
)}>
|
|
||||||
{day}
|
|
||||||
</span>
|
|
||||||
{isLoading ? (
|
|
||||||
<SkeletonText className="block h-5 w-60" />
|
|
||||||
) : isAvailable ? (
|
|
||||||
<div className="space-y-3 text-right">
|
|
||||||
{filterDays(index).map((dayRange, i) => (
|
|
||||||
<div key={i} className="flex items-center leading-4">
|
|
||||||
<span className="w-16 sm:w-28 sm:text-left">
|
|
||||||
{format(dayRange.startTime, timeFormat === 12)}
|
|
||||||
</span>
|
|
||||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
<hr />
|
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
|
||||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
|
||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
|
||||||
</span>
|
|
||||||
{!!schedule?.id && (
|
|
||||||
<Button
|
|
||||||
href={`/availability/${schedule.id}`}
|
|
||||||
disabled={isLoading}
|
|
||||||
color="minimal"
|
|
||||||
EndIcon={FiExternalLink}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{t("edit_availability")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
|
|
||||||
{t("availability")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => (
|
|
||||||
<AvailabilitySelect
|
|
||||||
value={field.value}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
onChange={(selected) => {
|
|
||||||
field.onChange(selected?.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<EventTypeScheduleDetails />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UseCommonScheduleSettingsToggle = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { resetField, setValue } = useFormContext<FormValues>();
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SettingsToggle
|
|
||||||
checked={!value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
onChange(!checked);
|
|
||||||
if (checked) {
|
|
||||||
resetField("schedule");
|
|
||||||
} else {
|
|
||||||
setValue("schedule", null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={t("choose_common_schedule_team_event")}
|
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
|
||||||
<EventTypeSchedule />
|
|
||||||
</SettingsToggle>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|
||||||
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
|
||||||
};
|
|
||||||
@@ -58,35 +58,35 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
|||||||
className="mb-0"
|
className="mb-0"
|
||||||
/>
|
/>
|
||||||
<div className="pt-6 text-sm">
|
<div className="pt-6 text-sm">
|
||||||
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
<div className="bg-subtle mb-6 rounded-md p-2">
|
||||||
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
<h1 className="text-emphasis mb-2 ml-1 font-medium">{t("available_variables")}</h1>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Event type title}`}</p>
|
||||||
<p className="text-gray-900">{t("event_name_info")}</p>
|
<p className="text-emphasis">{t("event_name_info")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Organiser}`}</p>
|
||||||
<p className="text-gray-900">{t("your_full_name")}</p>
|
<p className="text-emphasis">{t("your_full_name")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Scheduler}`}</p>
|
||||||
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
<p className="text-emphasis">{t("scheduler_full_name")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-1 flex font-normal">
|
<div className="mb-1 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Location}`}</p>
|
||||||
<p className="text-gray-900">{t("location_info")}</p>
|
<p className="text-emphasis">{t("location_info")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
||||||
<div
|
<div
|
||||||
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center dark:invert"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: "url(/calendar-preview.svg)",
|
backgroundImage: "url(/calendar-preview.svg)",
|
||||||
}}>
|
}}>
|
||||||
<div className="m-auto flex items-center justify-center self-stretch">
|
<div className="m-auto flex items-center justify-center self-stretch">
|
||||||
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
|
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
||||||
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
|
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
|
||||||
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ import { v5 as uuidv5 } from "uuid";
|
|||||||
|
|
||||||
import type { EventNameObjectType } from "@calcom/core/event";
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
import { getEventName } from "@calcom/core/event";
|
import { getEventName } from "@calcom/core/event";
|
||||||
|
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
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.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
@@ -77,11 +81,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
const eventNamePlaceholder = getEventName({
|
const eventNamePlaceholder = getEventName({
|
||||||
...eventNameObject,
|
...eventNameObject,
|
||||||
eventName: formMethods.watch("eventName"),
|
eventName: formMethods.watch("eventName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||||
|
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||||
@@ -99,7 +111,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Link
|
<Link
|
||||||
href="/apps/categories/calendar"
|
href="/apps/categories/calendar"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-sm text-gray-600 hover:text-gray-900">
|
className="hover:text-emphasis text-default text-sm">
|
||||||
{t("add_another_calendar")}
|
{t("add_another_calendar")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,44 +130,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{t("select_which_cal")}</p>
|
<p className="text-default text-sm">{t("select_which_cal")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
|
{...shouldLockDisableProps("eventName")}
|
||||||
placeholder={eventNamePlaceholder}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
StartIcon={FiEdit}
|
|
||||||
variant="icon"
|
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
size="sm"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
|
||||||
aria-label="edit custom name"
|
aria-label="edit custom name"
|
||||||
/>
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
title={t("booking_questions_title")}
|
title={t("booking_questions_title")}
|
||||||
description={t("booking_questions_description")}
|
description={t("booking_questions_description")}
|
||||||
addFieldLabel={t("add_a_booking_question")}
|
addFieldLabel={t("add_a_booking_question")}
|
||||||
formProp="bookingFields"
|
formProp="bookingFields"
|
||||||
|
{...shouldLockDisableProps("bookingFields")}
|
||||||
|
dataStore={{
|
||||||
|
options: {
|
||||||
|
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
|
eventType={eventType}
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
metadata={eventType.metadata}
|
metadata={eventType.metadata}
|
||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
onRequiresConfirmation={setRequiresConfirmation}
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="hideCalendarNotes"
|
name="hideCalendarNotes"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -163,13 +182,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("disable_notes")}
|
title={t("disable_notes")}
|
||||||
|
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||||
description={t("disable_notes_description")}
|
description={t("disable_notes_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -177,48 +197,43 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("redirect_success_booking")}
|
title={t("redirect_success_booking")}
|
||||||
|
{...successRedirectUrlLocked}
|
||||||
description={t("redirect_url_description")}
|
description={t("redirect_url_description")}
|
||||||
checked={redirectUrlVisible}
|
checked={redirectUrlVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
setRedirectUrlVisible(e);
|
setRedirectUrlVisible(e);
|
||||||
onChange(e ? value : "");
|
onChange(e ? value : "");
|
||||||
}}>
|
}}>
|
||||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||||
<div className="lg:-ml-2">
|
<div className="lg:-ml-2 lg:-mb-2">
|
||||||
<TextField
|
<TextField
|
||||||
|
className="w-full"
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
labelSrOnly
|
labelSrOnly
|
||||||
|
disabled={successRedirectUrlLocked.disabled}
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
required={redirectUrlVisible}
|
required={redirectUrlVisible}
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex">
|
<div
|
||||||
<Checkbox
|
className={classNames(
|
||||||
description={t("disable_success_page")}
|
"p-1 text-sm text-orange-600",
|
||||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
)}>
|
||||||
{...formMethods.register("metadata.disableSuccessPage")}
|
{t("redirect_url_warning")}
|
||||||
/>
|
|
||||||
{/*TODO: Extract it out into a component when used more than once*/}
|
|
||||||
{!IS_SELF_HOSTED && (
|
|
||||||
<Link href="https://cal.com/pricing" target="_blank">
|
|
||||||
<Badge variant="orange" className="ml-2">
|
|
||||||
Platform Only
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
|
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||||
description={t("private_link_description", { appName: APP_NAME })}
|
description={t("private_link_description", { appName: APP_NAME })}
|
||||||
checked={hashedLinkVisible}
|
checked={hashedLinkVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
@@ -240,6 +255,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
aria-label="copy link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
if (eventType.hashedLink) {
|
if (eventType.hashedLink) {
|
||||||
@@ -247,70 +266,76 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
} else {
|
} else {
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
<Copy className="h-4 w-4" />
|
||||||
type="button">
|
|
||||||
<FiCopy />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="seatsPerTimeSlotEnabled"
|
name="seatsPerTimeSlotEnabled"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<>
|
||||||
data-testid="offer-seats-toggle"
|
<SettingsToggle
|
||||||
title={t("offer_seats")}
|
data-testid="offer-seats-toggle"
|
||||||
description={t("offer_seats_description")}
|
title={t("offer_seats")}
|
||||||
checked={value}
|
{...seatsLocked}
|
||||||
onCheckedChange={(e) => {
|
description={t("offer_seats_description")}
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
checked={value}
|
||||||
if (e) {
|
disabled={noShowFeeEnabled}
|
||||||
toggleGuests(false);
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
setRequiresConfirmation(false);
|
if (e) {
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
toggleGuests(false);
|
||||||
} else {
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
setRequiresConfirmation(false);
|
||||||
toggleGuests(true);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
}
|
} else {
|
||||||
onChange(e);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
}}>
|
toggleGuests(true);
|
||||||
<Controller
|
}
|
||||||
name="seatsPerTimeSlot"
|
onChange(e);
|
||||||
control={formMethods.control}
|
}}>
|
||||||
defaultValue={eventType.seatsPerTimeSlot}
|
<Controller
|
||||||
render={({ field: { value, onChange } }) => (
|
name="seatsPerTimeSlot"
|
||||||
<div className="lg:-ml-2">
|
control={formMethods.control}
|
||||||
<TextField
|
defaultValue={eventType.seatsPerTimeSlot}
|
||||||
required
|
render={({ field: { value, onChange } }) => (
|
||||||
name="seatsPerTimeSlot"
|
<div className="lg:-ml-2">
|
||||||
labelSrOnly
|
<TextField
|
||||||
label={t("number_of_seats")}
|
required
|
||||||
type="number"
|
name="seatsPerTimeSlot"
|
||||||
defaultValue={value || 2}
|
labelSrOnly
|
||||||
min={1}
|
label={t("number_of_seats")}
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
type="number"
|
||||||
onChange={(e) => {
|
disabled={seatsLocked.disabled}
|
||||||
onChange(Math.abs(Number(e.target.value)));
|
defaultValue={value || 2}
|
||||||
}}
|
min={1}
|
||||||
/>
|
className="w-24"
|
||||||
<div className="mt-2">
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
<Checkbox
|
onChange={(e) => {
|
||||||
description={t("show_attendees")}
|
onChange(Math.abs(Number(e.target.value)));
|
||||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
}}
|
||||||
defaultChecked={!!eventType.seatsShowAttendees}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Checkbox
|
||||||
|
description={t("show_attendees")}
|
||||||
|
disabled={seatsLocked.disabled}
|
||||||
|
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||||
|
defaultChecked={!!eventType.seatsShowAttendees}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</SettingsToggle>
|
||||||
</SettingsToggle>
|
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
|
|||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
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, EmptyScreen } from "@calcom/ui";
|
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||||
import { FiGrid } from "@calcom/ui/components/icon";
|
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
|
{!installedApps?.length && isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_apps_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isLoading && !installedApps?.length ? (
|
{!isLoading && !installedApps?.length ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiGrid}
|
Icon={Grid}
|
||||||
headline={t("empty_installed_apps_headline")}
|
headline={t("empty_installed_apps_headline")}
|
||||||
description={t("empty_installed_apps_description")}
|
description={t("empty_installed_apps_description")}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button target="_blank" color="secondary" href="/apps">
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
{t("empty_installed_apps_button")}{" "}
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
</Button>
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button target="_blank" color="secondary" href="/apps">
|
||||||
|
{t("empty_installed_apps_button")}{" "}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
<div>
|
||||||
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2>
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
) : null}
|
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
<div className="before:border-0">
|
) : null}
|
||||||
{notInstalledApps?.map((app) => (
|
<div className="before:border-0">
|
||||||
<EventTypeAppCard
|
{notInstalledApps?.map((app) => (
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
<EventTypeAppCard
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
key={app.slug}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
app={app}
|
key={app.slug}
|
||||||
eventType={eventType}
|
app={app}
|
||||||
/>
|
eventType={eventType}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import { SchedulingType } from "@prisma/client";
|
||||||
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
|
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
|
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
export type AvailabilityOption = {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
isManaged?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<components.Option {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
{t("default")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
{t("default")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.SingleValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (date: Date, hour12: boolean) =>
|
||||||
|
Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hourCycle: hour12 ? "h12" : "h24",
|
||||||
|
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
|
||||||
|
|
||||||
|
const EventTypeScheduleDetails = ({
|
||||||
|
isManagedEventType,
|
||||||
|
selectedScheduleValue,
|
||||||
|
}: {
|
||||||
|
isManagedEventType: boolean;
|
||||||
|
selectedScheduleValue: AvailabilityOption | undefined;
|
||||||
|
}) => {
|
||||||
|
const { data: loggedInUser } = useMeQuery();
|
||||||
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
|
const { t, i18n } = useLocale();
|
||||||
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
|
const scheduleId = watch("schedule");
|
||||||
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
|
{
|
||||||
|
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
||||||
|
isManagedEventType,
|
||||||
|
},
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterDays = (dayNum: number) =>
|
||||||
|
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-default space-y-4 rounded border px-6 pb-4">
|
||||||
|
<ol className="table border-collapse text-sm">
|
||||||
|
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||||
|
const isAvailable = !!filterDays(index).length;
|
||||||
|
return (
|
||||||
|
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"w-20 font-medium sm:w-32 ",
|
||||||
|
!isAvailable ? "text-subtle line-through" : "text-default"
|
||||||
|
)}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonText className="block h-5 w-60" />
|
||||||
|
) : isAvailable ? (
|
||||||
|
<div className="space-y-3 text-right">
|
||||||
|
{filterDays(index).map((dayRange, i) => (
|
||||||
|
<div key={i} className="text-default flex items-center leading-4">
|
||||||
|
<span className="w-16 sm:w-28 sm:text-left">
|
||||||
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
|
</span>
|
||||||
|
<span className="ms-4">-</span>
|
||||||
|
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
|
||||||
|
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||||
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
|
</span>
|
||||||
|
{!!schedule?.id && !schedule.isManaged && (
|
||||||
|
<Button
|
||||||
|
href={`/availability/${schedule.id}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
color="minimal"
|
||||||
|
EndIcon={ExternalLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
{t("edit_availability")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
|
||||||
|
useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
const watchSchedule = watch("schedule");
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||||
|
|
||||||
|
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
|
||||||
|
onSuccess: ({ schedules }) => {
|
||||||
|
const options = schedules.map((schedule) => ({
|
||||||
|
value: schedule.id,
|
||||||
|
label: schedule.name,
|
||||||
|
isDefault: schedule.isDefault,
|
||||||
|
isManaged: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||||
|
if (isManagedEventType) {
|
||||||
|
options.push({
|
||||||
|
value: 0,
|
||||||
|
label: t("members_default_schedule"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
||||||
|
// the managed schedule option
|
||||||
|
if (
|
||||||
|
isChildrenManagedEventType &&
|
||||||
|
watchSchedule &&
|
||||||
|
!schedules.find((schedule) => schedule.id === watchSchedule)
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
value: watchSchedule,
|
||||||
|
label: eventType.scheduleName ?? t("default_schedule_name"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions(options);
|
||||||
|
|
||||||
|
const scheduleId = formMethods.getValues("schedule");
|
||||||
|
const value = options.find((option) =>
|
||||||
|
scheduleId
|
||||||
|
? option.value === scheduleId
|
||||||
|
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
formMethods.setValue("availability", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const availabilityValue = formMethods.watch("availability");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||||
|
{t("availability")}
|
||||||
|
{shouldLockIndicator("availability")}
|
||||||
|
</label>
|
||||||
|
{isLoading && <SelectSkeletonLoader />}
|
||||||
|
{!isLoading && (
|
||||||
|
<Controller
|
||||||
|
name="schedule"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
|
options={options}
|
||||||
|
isSearchable={false}
|
||||||
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
if (selected?.value) formMethods.setValue("availability", selected);
|
||||||
|
}}
|
||||||
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
|
value={availabilityValue}
|
||||||
|
components={{ Option, SingleValue }}
|
||||||
|
isMulti={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{availabilityValue?.value !== 0 ? (
|
||||||
|
<EventTypeScheduleDetails
|
||||||
|
selectedScheduleValue={availabilityValue}
|
||||||
|
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
isManagedEventType && (
|
||||||
|
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { resetField, setValue } = useFormContext<FormValues>();
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SettingsToggle
|
||||||
|
checked={!value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onChange(!checked);
|
||||||
|
if (checked) {
|
||||||
|
resetField("schedule");
|
||||||
|
} else {
|
||||||
|
setValue("schedule", null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t("choose_common_schedule_team_event")}
|
||||||
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
|
<EventTypeSchedule eventType={eventType} />
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventAvailabilityTab = ({
|
||||||
|
eventType,
|
||||||
|
isTeamEvent,
|
||||||
|
}: {
|
||||||
|
eventType: EventTypeSetup;
|
||||||
|
isTeamEvent: boolean;
|
||||||
|
}) => {
|
||||||
|
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
|
||||||
|
<UseCommonScheduleSettingsToggle eventType={eventType} />
|
||||||
|
) : (
|
||||||
|
<EventTypeSchedule eventType={eventType} />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
|
|||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import type { SingleValue } from "react-select";
|
import type { SingleValue } from "react-select";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
@@ -14,17 +15,8 @@ import findDurationType from "@calcom/lib/findDurationType";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
import {
|
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
Button,
|
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SettingsToggle,
|
|
||||||
TextField,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
const MinimumBookingNoticeInput = React.forwardRef<
|
const MinimumBookingNoticeInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
<div className="w-1/2 md:w-full">
|
<div className="w-1/2 md:w-full">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
|
disabled={passThroughProps.disabled}
|
||||||
defaultValue={minimumBookingNoticeDisplayValues.value}
|
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setMinimumBookingNoticeDisplayValues({
|
setMinimumBookingNoticeDisplayValues({
|
||||||
@@ -88,12 +81,14 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
label={t("minimum_booking_notice")}
|
label={t("minimum_booking_notice")}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
min={0}
|
||||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
/>
|
/>
|
||||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={passThroughProps.disabled}
|
||||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||||
defaultValue={durationTypeOptions.find(
|
defaultValue={durationTypeOptions.find(
|
||||||
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||||
@@ -145,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
defaultValue: periodType?.type,
|
defaultValue: periodType?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||||
|
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||||
|
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||||
|
|
||||||
|
const optionsPeriod = [
|
||||||
|
{ value: 1, label: t("calendar_days") },
|
||||||
|
{ value: 0, label: t("business_days") },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4 lg:space-y-8">
|
<div className="space-y-4 lg:space-y-8">
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
<Label htmlFor="beforeBufferTime">
|
||||||
|
{t("before_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="beforeBufferTime"
|
name="beforeBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -169,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -182,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
<Label htmlFor="afterBufferTime">
|
||||||
|
{t("after_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="afterBufferTime"
|
name="afterBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -201,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -216,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
<Label htmlFor="minimumBookingNotice">
|
||||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
{t("minimum_booking_notice")}
|
||||||
|
{shouldLockIndicator("minimumBookingNotice")}
|
||||||
|
</Label>
|
||||||
|
<MinimumBookingNoticeInput
|
||||||
|
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||||
|
{...formMethods.register("minimumBookingNotice")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
<Label htmlFor="slotInterval">
|
||||||
|
{t("slot_interval")}
|
||||||
|
{shouldLockIndicator("slotInterval")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="slotInterval"
|
name="slotInterval"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -238,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||||
}}
|
}}
|
||||||
@@ -253,13 +281,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="bookingLimits"
|
name="bookingLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_booking_frequency")}
|
title={t("limit_booking_frequency")}
|
||||||
|
{...bookingLimitsLocked}
|
||||||
description={t("limit_booking_frequency_description")}
|
description={t("limit_booking_frequency_description")}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
@@ -271,11 +300,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
formMethods.setValue("bookingLimits", {});
|
formMethods.setValue("bookingLimits", {});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
<IntervalLimitsManager
|
||||||
|
disabled={bookingLimitsLocked.disabled}
|
||||||
|
propertyName="bookingLimits"
|
||||||
|
defaultLimit={1}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="durationLimits"
|
name="durationLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -283,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_total_booking_duration")}
|
title={t("limit_total_booking_duration")}
|
||||||
description={t("limit_total_booking_duration_description")}
|
description={t("limit_total_booking_duration_description")}
|
||||||
|
{...durationLimitsLocked}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -296,13 +331,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<IntervalLimitsManager
|
<IntervalLimitsManager
|
||||||
propertyName="durationLimits"
|
propertyName="durationLimits"
|
||||||
defaultLimit={60}
|
defaultLimit={60}
|
||||||
|
disabled={durationLimitsLocked.disabled}
|
||||||
step={15}
|
step={15}
|
||||||
textFieldSuffix={t("minutes")}
|
textFieldSuffix={t("minutes")}
|
||||||
/>
|
/>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="periodType"
|
name="periodType"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -310,49 +346,64 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_future_bookings")}
|
title={t("limit_future_bookings")}
|
||||||
description={t("limit_future_bookings_description")}
|
description={t("limit_future_bookings_description")}
|
||||||
|
{...periodTypeLocked}
|
||||||
checked={value && 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}
|
||||||
value={watchPeriodType}
|
value={watchPeriodType}
|
||||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||||
{PERIOD_TYPES.map((period) => {
|
{PERIOD_TYPES.filter((opt) =>
|
||||||
|
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
|
||||||
|
).map((period) => {
|
||||||
if (period.type === "UNLIMITED") return null;
|
if (period.type === "UNLIMITED") return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-2 flex flex-wrap items-center text-sm",
|
"text-default mb-2 flex flex-wrap items-center text-sm",
|
||||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||||
)}
|
)}
|
||||||
key={period.type}>
|
key={period.type}>
|
||||||
<RadioGroup.Item
|
{!periodTypeLocked.disabled && (
|
||||||
id={period.type}
|
<RadioGroup.Item
|
||||||
value={period.type}
|
id={period.type}
|
||||||
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">
|
value={period.type}
|
||||||
<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" />
|
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
</RadioGroup.Item>
|
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
)}
|
||||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||||
{period.type === "ROLLING" && (
|
{period.type === "ROLLING" && (
|
||||||
<div className="flex h-9">
|
<div className="flex items-center">
|
||||||
<Input
|
<TextField
|
||||||
|
labelSrOnly
|
||||||
type="number"
|
type="number"
|
||||||
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
defaultValue={eventType.periodDays || 30}
|
defaultValue={eventType.periodDays || 30}
|
||||||
/>
|
/>
|
||||||
<select
|
<Select
|
||||||
id=""
|
options={optionsPeriod}
|
||||||
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
isSearchable={false}
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
isDisabled={periodTypeLocked.disabled}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
onChange={(opt) => {
|
||||||
<option value="1">{t("calendar_days")}</option>
|
formMethods.setValue(
|
||||||
<option value="0">{t("business_days")}</option>
|
"periodCountCalendarDays",
|
||||||
</select>
|
opt?.value.toString() as "0" | "1"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
optionsPeriod.find(
|
||||||
|
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
||||||
|
) ?? optionsPeriod[0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.type === "RANGE" && (
|
{period.type === "RANGE" && (
|
||||||
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||||
<Controller
|
<Controller
|
||||||
name="periodDates"
|
name="periodDates"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -361,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
startDate={formMethods.getValues("periodDates").startDate}
|
startDate={formMethods.getValues("periodDates").startDate}
|
||||||
endDate={formMethods.getValues("periodDates").endDate}
|
endDate={formMethods.getValues("periodDates").endDate}
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
onDatesChange={({ startDate, endDate }) => {
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
formMethods.setValue("periodDates", {
|
formMethods.setValue("periodDates", {
|
||||||
startDate,
|
startDate,
|
||||||
@@ -372,9 +424,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.suffix ? (
|
{period.suffix ? <span className="ms-2 me-2"> {period.suffix}</span> : null}
|
||||||
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2"> {period.suffix}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -401,6 +451,7 @@ type IntervalLimitItemProps = {
|
|||||||
step: number;
|
step: number;
|
||||||
value: number;
|
value: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||||
hasDeleteButton?: boolean;
|
hasDeleteButton?: boolean;
|
||||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||||
@@ -415,6 +466,7 @@ const IntervalLimitItem = ({
|
|||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
selectOptions,
|
selectOptions,
|
||||||
hasDeleteButton,
|
hasDeleteButton,
|
||||||
|
disabled,
|
||||||
onDelete,
|
onDelete,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onIntervalSelect,
|
onIntervalSelect,
|
||||||
@@ -424,8 +476,10 @@ const IntervalLimitItem = ({
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
|
className="mb-0 !h-auto"
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
|
disabled={disabled}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
@@ -435,11 +489,12 @@ const IntervalLimitItem = ({
|
|||||||
<Select
|
<Select
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={disabled}
|
||||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||||
onChange={onIntervalSelect}
|
onChange={onIntervalSelect}
|
||||||
/>
|
/>
|
||||||
{hasDeleteButton && (
|
{hasDeleteButton && !disabled && (
|
||||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -450,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
|
|||||||
defaultLimit: number;
|
defaultLimit: number;
|
||||||
step: number;
|
step: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||||
@@ -457,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
defaultLimit,
|
defaultLimit,
|
||||||
step,
|
step,
|
||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
|
disabled,
|
||||||
}: IntervalLimitsManagerProps<K>) => {
|
}: IntervalLimitsManagerProps<K>) => {
|
||||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||||
const watchIntervalLimits = watch(propertyName);
|
const watchIntervalLimits = watch(propertyName);
|
||||||
@@ -506,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
limitKey={limitKey}
|
limitKey={limitKey}
|
||||||
step={step}
|
step={step}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
textFieldSuffix={textFieldSuffix}
|
textFieldSuffix={textFieldSuffix}
|
||||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||||
@@ -536,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
|
||||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
|
||||||
{t("add_limit")}
|
{t("add_limit")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
|
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -4,20 +4,31 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import type { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
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 { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import {
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
Button,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SettingsToggle,
|
||||||
|
Skeleton,
|
||||||
|
TextField,
|
||||||
|
Editor,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
@@ -35,14 +46,57 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||||
for (const locationType of options) {
|
const locationAvailable =
|
||||||
for (const location of locationType.options) {
|
props.eventType.locations &&
|
||||||
if (location.value === type && location.disabled === false) {
|
props.eventType.locations.length > 0 &&
|
||||||
return location;
|
props.locationOptions.some((op) =>
|
||||||
}
|
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
|
||||||
}
|
);
|
||||||
}
|
const locationDetails = props.eventType.locations &&
|
||||||
|
props.eventType.locations.length > 0 &&
|
||||||
|
!locationAvailable && {
|
||||||
|
slug: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", "-")
|
||||||
|
.replace("_video", ""),
|
||||||
|
name: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", " ")
|
||||||
|
.replace("_video", "")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word[0].toUpperCase() + word.slice(1))
|
||||||
|
.join(" "),
|
||||||
|
};
|
||||||
|
return { locationAvailable, locationDetails };
|
||||||
|
};
|
||||||
|
interface DescriptionEditorProps {
|
||||||
|
description?: string | null;
|
||||||
|
editable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [mounted, setIsMounted] = useState(false);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { description } = props;
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted ? (
|
||||||
|
<Editor
|
||||||
|
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
||||||
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
editable={props.editable}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<SkeletonText className="block h-24 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
@@ -53,12 +107,16 @@ export const EventSetupTab = (
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
const { eventType, team, destinationCalendar } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
|
|
||||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
value: mins,
|
value: mins,
|
||||||
label: t("multiple_duration_mins", { count: mins }),
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
@@ -143,6 +201,13 @@ export const EventSetupTab = (
|
|||||||
resolver: zodResolver(locationFormSchema),
|
resolver: zodResolver(locationFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||||
|
useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
const Locations = () => {
|
const Locations = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -157,6 +222,12 @@ export const EventSetupTab = (
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultValue = isManagedEventType
|
||||||
|
? locationOptions.find((op) => op.label === t("default"))?.options[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { locationDetails, locationAvailable } = getLocationInfo(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
@@ -164,6 +235,8 @@ export const EventSetupTab = (
|
|||||||
<LocationSelect
|
<LocationSelect
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
|
isDisabled={shouldLockDisableProps("locations").disabled}
|
||||||
|
defaultValue={defaultValue}
|
||||||
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"
|
||||||
menuPlacement="auto"
|
menuPlacement="auto"
|
||||||
@@ -199,7 +272,7 @@ export const EventSetupTab = (
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
className="border-default text-default mb-2 rounded-md border py-1.5 px-2 hover:cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
@@ -207,7 +280,7 @@ export const EventSetupTab = (
|
|||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -221,11 +294,11 @@ export const EventSetupTab = (
|
|||||||
openLocationModal(location.type);
|
openLocationModal(location.type);
|
||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||||
<FiEdit2 className="h-4 w-4" />
|
<Edit2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||||
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,8 +309,8 @@ export const EventSetupTab = (
|
|||||||
(location) =>
|
(location) =>
|
||||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
) && (
|
) && (
|
||||||
<div className="flex text-sm text-gray-600">
|
<div className="text-default flex text-sm">
|
||||||
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
<Trans i18nKey="event_type_requres_google_cal">
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
<p>
|
<p>
|
||||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
@@ -251,11 +324,19 @@ export const EventSetupTab = (
|
|||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && (
|
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
|
||||||
|
<p className="pl-1 text-sm leading-none text-red-600">
|
||||||
|
{t("app_not_connected", { appName: locationDetails.name })}{" "}
|
||||||
|
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
|
||||||
|
{t("connect_now")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-location"
|
data-testid="add-location"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
onClick={() => setShowLocationModal(true)}>
|
onClick={() => setShowLocationModal(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
@@ -268,27 +349,33 @@ export const EventSetupTab = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lengthLockedProps = shouldLockDisableProps("length");
|
||||||
|
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("title")}
|
label={t("title")}
|
||||||
|
{...shouldLockDisableProps("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("description")}</Label>
|
<Label>
|
||||||
<Editor
|
{t("description")}
|
||||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
{shouldLockIndicator("description")}
|
||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
</Label>
|
||||||
excludedToolbarItems={["blockType"]}
|
<DescriptionEditor
|
||||||
placeholder={t("quick_video_meeting")}
|
description={eventType?.description}
|
||||||
|
editable={!descriptionLockedProps.disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
|
{...shouldLockDisableProps("slug")}
|
||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
@@ -341,12 +428,14 @@ export const EventSetupTab = (
|
|||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("default_duration")}
|
{t("default_duration")}
|
||||||
|
{shouldLockIndicator("length")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Select
|
<Select
|
||||||
value={defaultDuration}
|
value={defaultDuration}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
name="length"
|
name="length"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
|
isDisabled={lengthLockedProps.disabled}
|
||||||
noOptionsMessage={() => t("default_duration_no_options")}
|
noOptionsMessage={() => t("default_duration_no_options")}
|
||||||
options={selectedMultipleDuration}
|
options={selectedMultipleDuration}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
@@ -362,32 +451,36 @@ export const EventSetupTab = (
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
|
{...lengthLockedProps}
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
{...formMethods.register("length")}
|
{...formMethods.register("length")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
{!lengthLockedProps.disabled && (
|
||||||
<SettingsToggle
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
title={t("allow_booker_to_select_duration")}
|
<SettingsToggle
|
||||||
checked={multipleDuration !== undefined}
|
title={t("allow_booker_to_select_duration")}
|
||||||
onCheckedChange={() => {
|
checked={multipleDuration !== undefined}
|
||||||
if (multipleDuration !== undefined) {
|
onCheckedChange={() => {
|
||||||
setMultipleDuration(undefined);
|
if (multipleDuration !== undefined) {
|
||||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
setMultipleDuration(undefined);
|
||||||
formMethods.setValue("length", eventType.length);
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
} else {
|
formMethods.setValue("length", eventType.length);
|
||||||
setMultipleDuration([]);
|
} else {
|
||||||
formMethods.setValue("metadata.multipleDuration", []);
|
setMultipleDuration([]);
|
||||||
formMethods.setValue("length", 0);
|
formMethods.setValue("metadata.multipleDuration", []);
|
||||||
}
|
formMethods.setValue("length", 0);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
|
{shouldLockIndicator("locations")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
@@ -401,6 +494,7 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
|
isTeamEvent={!!team}
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
@@ -7,24 +7,46 @@ import type { Options } from "react-select";
|
|||||||
|
|
||||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
|
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||||
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 { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IUserToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
|
||||||
value: `${id || ""}`,
|
value: `${id || ""}`,
|
||||||
label: `${name || ""}`,
|
label: `${name || ""}`,
|
||||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapMemberToChildrenOption = (
|
||||||
|
member: EventTypeSetupProps["teamMembers"][number],
|
||||||
|
slug: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
hidden: false,
|
||||||
|
created: false,
|
||||||
|
owner: {
|
||||||
|
id: member.id,
|
||||||
|
name: member.name ?? "",
|
||||||
|
email: member.email,
|
||||||
|
username: member.username ?? "",
|
||||||
|
membership: member.membership,
|
||||||
|
eventTypeSlugs: member.eventTypes ?? [],
|
||||||
|
},
|
||||||
|
value: `${member.id ?? ""}`,
|
||||||
|
label: member.name ?? "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
if (a.label < b.label) {
|
if (a.label < b.label) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypesList = ({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
value: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
|
||||||
|
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
|
||||||
|
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-5">
|
||||||
|
<div>
|
||||||
|
<Label>{t("assign_to")}</Label>
|
||||||
|
<ChildrenEventTypeSelect
|
||||||
|
onChange={(options) => {
|
||||||
|
onChange &&
|
||||||
|
onChange(
|
||||||
|
options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
|
||||||
|
controlShouldRenderValue={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CheckedHostField = ({
|
const CheckedHostField = ({
|
||||||
labelText,
|
labelText,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -52,7 +108,7 @@ const CheckedHostField = ({
|
|||||||
options?: Options<CheckedSelectOption>;
|
options?: Options<CheckedSelectOption>;
|
||||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
<div className="bg-muted flex flex-col space-y-5 p-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>{labelText}</Label>
|
<Label>{labelText}</Label>
|
||||||
<CheckedTeamSelect
|
<CheckedTeamSelect
|
||||||
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypes = ({
|
||||||
|
childrenEventTypeOptions,
|
||||||
|
}: {
|
||||||
|
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Controller<FormValues>
|
||||||
|
name="children"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Hosts = ({
|
const Hosts = ({
|
||||||
teamMembers,
|
teamMembers,
|
||||||
}: {
|
}: {
|
||||||
@@ -189,6 +260,7 @@ const Hosts = ({
|
|||||||
/>*/}
|
/>*/}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
MANAGED: <></>,
|
||||||
};
|
};
|
||||||
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
||||||
}}
|
}}
|
||||||
@@ -196,7 +268,11 @@ const Hosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
export const EventTeamTab = ({
|
||||||
|
team,
|
||||||
|
teamMembers,
|
||||||
|
eventType,
|
||||||
|
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const schedulingTypeOptions: {
|
const schedulingTypeOptions: {
|
||||||
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
const childrenEventTypeOptions = teamMembers.map((member) => {
|
||||||
|
return mapMemberToChildrenOption(member, eventType.slug);
|
||||||
|
});
|
||||||
|
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && !isManagedEventType && (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Label>{t("scheduling_type")}</Label>
|
<Label>{t("scheduling_type")}</Label>
|
||||||
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
<Hosts teamMembers={teamMembersOptions} />
|
<Hosts teamMembers={teamMembersOptions} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{team && isManagedEventType && (
|
||||||
|
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import type { Webhook } from "@prisma/client";
|
||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Webhook } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { Plus, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const EventTeamWebhooksTab = ({
|
export const EventTeamWebhooksTab = ({
|
||||||
eventType,
|
eventType,
|
||||||
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
data-testid="new_webhook"
|
data-testid="new_webhook"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
onClick={() => setCreateModalOpen(true)}>
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
{t("new_webhook")}
|
{t("new_webhook")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && webhooks && !isLoading && (
|
{team && webhooks && !isLoading && (
|
||||||
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<>
|
<>
|
||||||
|
{isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_webhooks_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-8 rounded-md border">
|
<div className="mb-2 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
key={webhook.id}
|
key={webhook.id}
|
||||||
webhook={webhook}
|
webhook={webhook}
|
||||||
lastItem={webhooks.length === index + 1}
|
lastItem={webhooks.length === index + 1}
|
||||||
|
canEditWebhook={!webhookLockedStatus.disabled}
|
||||||
onEditWebhook={() => {
|
onEditWebhook={() => {
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
setWebhookToEdit(webhook);
|
setWebhookToEdit(webhook);
|
||||||
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
|
|||||||
Icon={TbWebhook}
|
Icon={TbWebhook}
|
||||||
headline={t("create_your_first_webhook")}
|
headline={t("create_your_first_webhook")}
|
||||||
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||||
buttonRaw={<NewWebhookButton />}
|
buttonRaw={
|
||||||
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<NewWebhookButton />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export type EventTypeDescriptionSafeProps = {
|
export type EventTypeDescriptionSafeProps = {
|
||||||
eventType: { description: string | null };
|
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
if (eventType.description)
|
||||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||||
return <div {...props} />;
|
return <div {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { SchedulingType } from "@prisma/client";
|
||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState, Suspense } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -32,22 +35,23 @@ import {
|
|||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiLink,
|
Link as LinkIcon,
|
||||||
FiCalendar,
|
Calendar,
|
||||||
FiClock,
|
Clock,
|
||||||
FiSliders,
|
Sliders,
|
||||||
FiRepeat,
|
Repeat,
|
||||||
FiGrid,
|
Grid,
|
||||||
FiZap,
|
Zap,
|
||||||
FiUsers,
|
Users,
|
||||||
FiExternalLink,
|
ExternalLink,
|
||||||
FiCode,
|
Code,
|
||||||
FiTrash,
|
Trash,
|
||||||
FiMoreHorizontal,
|
MoreHorizontal,
|
||||||
FiLoader,
|
Loader,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -60,6 +64,7 @@ type Props = {
|
|||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
formMethods: UseFormReturn<FormValues>;
|
formMethods: UseFormReturn<FormValues>;
|
||||||
isUpdateMutationLoading?: boolean;
|
isUpdateMutationLoading?: boolean;
|
||||||
|
availability?: AvailabilityOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNavigation(props: {
|
function getNavigation(props: {
|
||||||
@@ -68,8 +73,10 @@ function getNavigation(props: {
|
|||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||||
|
props;
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -77,44 +84,38 @@ function getNavigation(props: {
|
|||||||
{
|
{
|
||||||
name: "event_setup_tab_title",
|
name: "event_setup_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||||
icon: FiLink,
|
icon: LinkIcon,
|
||||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "availability",
|
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
|
||||||
icon: FiCalendar,
|
|
||||||
info: `default_schedule_name`, // TODO: Get this from props
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
info: `event_limit_tab_description`,
|
info: `event_limit_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_advanced_tab_title",
|
name: "event_advanced_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||||
icon: FiSliders,
|
icon: Sliders,
|
||||||
info: `event_advanced_tab_description`,
|
info: `event_advanced_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recurring",
|
name: "recurring",
|
||||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||||
icon: FiRepeat,
|
icon: Repeat,
|
||||||
info: `recurring_event_tab_description`,
|
info: `recurring_event_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "apps",
|
name: "apps",
|
||||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
//TODO: Handle proper translation with count handling
|
//TODO: Handle proper translation with count handling
|
||||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "workflows",
|
name: "workflows",
|
||||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||||
icon: FiZap,
|
icon: Zap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -131,13 +132,17 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
isUpdateMutationLoading,
|
isUpdateMutationLoading,
|
||||||
formMethods,
|
formMethods,
|
||||||
|
availability,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
|
const hasPermsToDelete =
|
||||||
|
currentUserMembership?.role !== "MEMBER" ||
|
||||||
|
!currentUserMembership ||
|
||||||
|
eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
|
|
||||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -157,23 +162,56 @@ function EventTypeSingleLayout({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
const navigation = getNavigation({
|
let navigation = getNavigation({
|
||||||
t,
|
t,
|
||||||
eventType,
|
eventType,
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
|
});
|
||||||
|
navigation.splice(1, 0, {
|
||||||
|
name: "availability",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
|
icon: Calendar,
|
||||||
|
info:
|
||||||
|
isManagedEventType || isChildrenManagedEventType
|
||||||
|
? eventType.schedule === null
|
||||||
|
? "Member's default schedule"
|
||||||
|
: isChildrenManagedEventType
|
||||||
|
? `${
|
||||||
|
eventType.scheduleName
|
||||||
|
? `${eventType.scheduleName} - ${t("managed")}`
|
||||||
|
: `default_schedule_name`
|
||||||
|
}`
|
||||||
|
: eventType.scheduleName ?? `default_schedule_name`
|
||||||
|
: `default_schedule_name`,
|
||||||
});
|
});
|
||||||
// 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: "assignment",
|
name: "assignment",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
||||||
|
isManagedEventType
|
||||||
|
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
|
navigation = navigation.slice(0, -2);
|
||||||
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
@@ -182,13 +220,14 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
||||||
|
|
||||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
||||||
|
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
@@ -197,86 +236,96 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
|
{!eventType.metadata.managedEventConfig && (
|
||||||
<Skeleton
|
<>
|
||||||
as={Label}
|
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
|
||||||
htmlFor="hiddenSwitch"
|
<Skeleton
|
||||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
as={Label}
|
||||||
{t("hide_from_profile")}
|
htmlFor="hiddenSwitch"
|
||||||
</Skeleton>
|
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||||
<Switch
|
{t("hide_from_profile")}
|
||||||
id="hiddenSwitch"
|
</Skeleton>
|
||||||
checked={formMethods.watch("hidden")}
|
<Switch
|
||||||
onCheckedChange={(e) => {
|
id="hiddenSwitch"
|
||||||
formMethods.setValue("hidden", e);
|
checked={formMethods.watch("hidden")}
|
||||||
}}
|
onCheckedChange={(e) => {
|
||||||
/>
|
formMethods.setValue("hidden", e);
|
||||||
</div>
|
}}
|
||||||
<VerticalDivider className="hidden lg:block" />
|
/>
|
||||||
|
</div>
|
||||||
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
||||||
<ButtonGroup combined containerProps={{ className: "border-gray-300 hidden lg:flex" }}>
|
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
|
||||||
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
{!isManagedEventType && (
|
||||||
<Tooltip content={t("preview")}>
|
<>
|
||||||
<Button
|
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
||||||
color="secondary"
|
<Tooltip content={t("preview")}>
|
||||||
data-testid="preview-button"
|
<Button
|
||||||
target="_blank"
|
color="secondary"
|
||||||
variant="icon"
|
data-testid="preview-button"
|
||||||
href={permalink}
|
target="_blank"
|
||||||
rel="noreferrer"
|
variant="icon"
|
||||||
StartIcon={FiExternalLink}
|
href={permalink}
|
||||||
/>
|
rel="noreferrer"
|
||||||
</Tooltip>
|
StartIcon={ExternalLink}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EmbedButton
|
<EmbedButton
|
||||||
embedUrl={encodeURIComponent(embedLink)}
|
embedUrl={encodeURIComponent(embedLink)}
|
||||||
StartIcon={FiCode}
|
StartIcon={Code}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
</>
|
||||||
color="secondary"
|
)}
|
||||||
variant="icon"
|
{!isChildrenManagedEventType && (
|
||||||
StartIcon={FiTrash}
|
<Button
|
||||||
tooltip={t("delete")}
|
color="destructive"
|
||||||
disabled={!hasPermsToDelete}
|
variant="icon"
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
StartIcon={Trash}
|
||||||
/>
|
tooltip={t("delete")}
|
||||||
|
disabled={!hasPermsToDelete}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<VerticalDivider className="hidden lg:block" />
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiExternalLink}
|
StartIcon={ExternalLink}
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
@@ -284,18 +333,18 @@ function EventTypeSingleLayout({
|
|||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
color="destructive"
|
color="destructive"
|
||||||
StartIcon={FiTrash}
|
StartIcon={Trash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
|
<div className="sm:hover:bg-subtle flex h-9 flex-row items-center justify-between py-2 px-4">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
@@ -312,7 +361,7 @@ function EventTypeSingleLayout({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<div className="border-l-2 border-gray-300" />
|
<div className="border-default border-l-2" />
|
||||||
<Button
|
<Button
|
||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -323,7 +372,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Suspense fallback={<FiLoader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
@@ -331,6 +380,8 @@ function EventTypeSingleLayout({
|
|||||||
tabs={EventTypeTabs}
|
tabs={EventTypeTabs}
|
||||||
sticky
|
sticky
|
||||||
linkProps={{ shallow: true }}
|
linkProps={{ shallow: true }}
|
||||||
|
itemClassname="items-start"
|
||||||
|
iconClassName="md:mt-px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||||
@@ -339,7 +390,7 @@ function EventTypeSingleLayout({
|
|||||||
<div className="w-full ltr:mr-2 rtl:ml-2">
|
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
@@ -351,14 +402,25 @@ function EventTypeSingleLayout({
|
|||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_event_type")}
|
title={t(`delete${isManagedEvent}_event_type`)}
|
||||||
confirmBtnText={t("confirm_delete_event_type")}
|
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||||
loadingText={t("confirm_delete_event_type")}
|
loadingText={t(`confirm_delete_event_type`)}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteMutation.mutate({ id: eventType.id });
|
deleteMutation.mutate({ id: eventType.id });
|
||||||
}}>
|
}}>
|
||||||
{t("delete_event_type_description")}
|
<p className="mt-5">
|
||||||
|
<Trans
|
||||||
|
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||||
|
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||||
|
<ul>
|
||||||
|
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||||
|
<li>
|
||||||
|
Anyone who they've shared their link with will no longer be able to book using it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<EmbedDialog />
|
<EmbedDialog />
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
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, Select, SettingsToggle } from "@calcom/ui";
|
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
type RecurringEventControllerProps = {
|
type RecurringEventControllerProps = {
|
||||||
recurringEvent: RecurringEvent | null;
|
eventType: EventTypeSetup;
|
||||||
paymentEnabled: boolean;
|
paymentEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RecurringEventController({
|
export default function RecurringEventController({
|
||||||
recurringEvent,
|
eventType,
|
||||||
paymentEnabled,
|
paymentEnabled,
|
||||||
}: RecurringEventControllerProps) {
|
}: RecurringEventControllerProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
|
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||||
|
eventType.recurringEvent
|
||||||
|
);
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||||
@@ -28,6 +31,14 @@ export default function RecurringEventController({
|
|||||||
value: value.toString(),
|
value: value.toString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className={!paymentEnabled ? "w-full" : ""}>
|
<div className={!paymentEnabled ? "w-full" : ""}>
|
||||||
@@ -37,6 +48,7 @@ export default function RecurringEventController({
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("recurring_event")}
|
title={t("recurring_event")}
|
||||||
|
{...recurringLocked}
|
||||||
description={t("recurring_event_description")}
|
description={t("recurring_event_description")}
|
||||||
checked={recurringEventState !== null}
|
checked={recurringEventState !== null}
|
||||||
data-testid="recurring-event-check"
|
data-testid="recurring-event-check"
|
||||||
@@ -45,7 +57,7 @@ export default function RecurringEventController({
|
|||||||
formMethods.setValue("recurringEvent", null);
|
formMethods.setValue("recurringEvent", null);
|
||||||
setRecurringEventState(null);
|
setRecurringEventState(null);
|
||||||
} else {
|
} else {
|
||||||
const newVal = recurringEvent || {
|
const newVal = eventType.recurringEvent || {
|
||||||
interval: 1,
|
interval: 1,
|
||||||
count: 12,
|
count: 12,
|
||||||
freq: Frequency.WEEKLY,
|
freq: Frequency.WEEKLY,
|
||||||
@@ -57,12 +69,13 @@ export default function RecurringEventController({
|
|||||||
{recurringEventState && (
|
{recurringEventState && (
|
||||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="mb-0"
|
||||||
defaultValue={recurringEventState.interval}
|
defaultValue={recurringEventState.interval}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
@@ -77,7 +90,8 @@ export default function RecurringEventController({
|
|||||||
options={recurringEventFreqOptions}
|
options={recurringEventFreqOptions}
|
||||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
|
||||||
|
isDisabled={recurringLocked.disabled}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -89,13 +103,14 @@ export default function RecurringEventController({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultValue={recurringEventState.count}
|
defaultValue={recurringEventState.count}
|
||||||
|
className="mb-0"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -105,7 +120,7 @@ export default function RecurringEventController({
|
|||||||
setRecurringEventState(newVal);
|
setRecurringEventState(newVal);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
|
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
|
||||||
{t("events", {
|
{t("events", {
|
||||||
count: recurringEventState.count,
|
count: recurringEventState.count,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import type { UnitTypeLongPlural } from "dayjs";
|
import type { UnitTypeLongPlural } from "dayjs";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
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 type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Input, Label, SettingsToggle } from "@calcom/ui";
|
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean;
|
||||||
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
seatsEnabled: boolean;
|
seatsEnabled: boolean;
|
||||||
|
eventType: EventTypeSetup;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RequiresConfirmationController({
|
export default function RequiresConfirmationController({
|
||||||
metadata,
|
metadata,
|
||||||
|
eventType,
|
||||||
requiresConfirmation,
|
requiresConfirmation,
|
||||||
onRequiresConfirmation,
|
onRequiresConfirmation,
|
||||||
seatsEnabled,
|
seatsEnabled,
|
||||||
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
|
|||||||
}
|
}
|
||||||
}, [requiresConfirmation]);
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: t("minute_timeUnit"), value: "minutes" },
|
||||||
|
{ label: t("hour_timeUnit"), value: "hours" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultValue = options.find(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("requires_confirmation")}
|
title={t("requires_confirmation")}
|
||||||
disabled={seatsEnabled}
|
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
|
||||||
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
||||||
description={t("requires_confirmation_description")}
|
description={t("requires_confirmation_description")}
|
||||||
checked={requiresConfirmation}
|
checked={requiresConfirmation}
|
||||||
|
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||||
onCheckedChange={(val) => {
|
onCheckedChange={(val) => {
|
||||||
formMethods.setValue("requiresConfirmation", val);
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
onRequiresConfirmation(val);
|
onRequiresConfirmation(val);
|
||||||
@@ -77,73 +99,82 @@ export default function RequiresConfirmationController({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
<div className="flex flex-col flex-wrap justify-start gap-y-2">
|
||||||
<div className="flex items-center">
|
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<RadioGroup.Item
|
<RadioField
|
||||||
|
label={t("always_requires_confirmation")}
|
||||||
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
id="always"
|
id="always"
|
||||||
value="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>
|
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<Label htmlFor="always" className="!m-0 flex items-center">
|
<RadioField
|
||||||
{t("always_requires_confirmation")}
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
</Label>
|
className="items-center"
|
||||||
</div>
|
label={
|
||||||
<div className="flex items-center">
|
<>
|
||||||
<RadioGroup.Item
|
<Trans
|
||||||
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
|
defaults="When booked with less than <time></time> notice"
|
||||||
|
components={{
|
||||||
|
time: (
|
||||||
|
<div className="mx-2 inline-flex">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = Number(evt.target?.value);
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
unit:
|
||||||
|
requiresConfirmationSetup?.unit ??
|
||||||
|
defaultRequiresConfirmationSetup.unit,
|
||||||
|
time: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold.time",
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
||||||
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
|
||||||
|
)}>
|
||||||
|
<Select
|
||||||
|
inputId="notice"
|
||||||
|
options={options}
|
||||||
|
isSearchable={false}
|
||||||
|
isDisabled={requiresConfirmationLockedProps.disabled}
|
||||||
|
className="ml-2"
|
||||||
|
onChange={(opt) => {
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
time:
|
||||||
|
requiresConfirmationSetup?.time ??
|
||||||
|
defaultRequiresConfirmationSetup.time,
|
||||||
|
unit: opt?.value as UnitTypeLongPlural,
|
||||||
|
});
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold.unit",
|
||||||
|
opt?.value as UnitTypeLongPlural
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
id="notice"
|
id="notice"
|
||||||
value="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>
|
</div>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
import { Clock, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +11,7 @@ function SkeletonLoader() {
|
|||||||
<SkeletonText className="h-4 w-24" />
|
<SkeletonText className="h-4 w-24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="border-subtle bg-default divide-subtle divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import { Button } from "@calcom/ui";
|
|||||||
interface ICalendarItem {
|
interface ICalendarItem {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
imageSrc: string;
|
logo: string;
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarItem = (props: ICalendarItem) => {
|
const CalendarItem = (props: ICalendarItem) => {
|
||||||
const { title, imageSrc, type } = props;
|
const { title, logo, type } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center justify-between p-5">
|
<div className="flex flex-row items-center justify-between p-5">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<img src={imageSrc} alt={title} className="h-8 w-8" />
|
<img src={logo} alt={title} className="h-8 w-8" />
|
||||||
<p className="text-sm font-bold">{title}</p>
|
<p className="text-sm font-bold">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
<InstallAppButtonWithoutPlanCheck
|
<InstallAppButtonWithoutPlanCheck
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
|||||||
<p className="font-sans text-sm font-bold leading-5">
|
<p className="font-sans text-sm font-bold leading-5">
|
||||||
{name}
|
{name}
|
||||||
{/* Temporarily removed till we use it on another place */}
|
{/* Temporarily removed till we use it on another place */}
|
||||||
{/* <span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
|
{/* <span className="mx-1 rounded-[4px] bg-success py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
|
||||||
{t("default")}
|
{t("default")}
|
||||||
</span> */}
|
</span> */}
|
||||||
</p>
|
</p>
|
||||||
<div className="fle-row flex">
|
<div className="fle-row flex">
|
||||||
<span
|
<span
|
||||||
title={externalId}
|
title={externalId}
|
||||||
className="max-w-44 mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm text-gray-500">
|
className="max-w-44 text-subtle mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm">
|
||||||
{externalId}{" "}
|
{externalId}{" "}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,11 +44,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
|||||||
{/* <Button
|
{/* <Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto flex rounded-md border border-gray-200 py-[10x] px-4 font-sans text-sm">
|
className="ml-auto flex rounded-md border border-subtle py-[10x] px-4 font-sans text-sm">
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button> */}
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full border-b border-gray-200" />
|
<div className="border-subtle h-[1px] w-full border-b" />
|
||||||
<div>
|
<div>
|
||||||
<ul className="p-4">
|
<ul className="p-4">
|
||||||
{calendars?.map((calendar, i) => (
|
{calendars?.map((calendar, i) => (
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterInputs } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
|
||||||
|
|
||||||
interface ICreateEventsOnCalendarSelectProps {
|
interface ICreateEventsOnCalendarSelectProps {
|
||||||
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
||||||
}
|
}
|
||||||
@@ -17,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
|
|||||||
<>
|
<>
|
||||||
<div className="mt-6 flex flex-row">
|
<div className="mt-6 flex flex-row">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
|
<label htmlFor="createEventsOn" className="text-default flex text-sm font-medium">
|
||||||
{t("create_events_on")}
|
{t("create_events_on")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
firstCalendar &&
|
firstCalendar &&
|
||||||
firstCalendar.integration &&
|
firstCalendar.integration &&
|
||||||
firstCalendar.integration.title &&
|
firstCalendar.integration.title &&
|
||||||
firstCalendar.integration.imageSrc && (
|
firstCalendar.integration.logo && (
|
||||||
<>
|
<>
|
||||||
<List className="rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
|
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
|
||||||
<ConnectedCalendarItem
|
<ConnectedCalendarItem
|
||||||
key={firstCalendar.integration.title}
|
key={firstCalendar.integration.title}
|
||||||
name={firstCalendar.integration.title}
|
name={firstCalendar.integration.title}
|
||||||
logo={firstCalendar.integration.imageSrc}
|
logo={firstCalendar.integration.logo}
|
||||||
externalId={
|
externalId={
|
||||||
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
|
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
|
||||||
? firstCalendar.calendars[0].externalId
|
? firstCalendar.calendars[0].externalId
|
||||||
@@ -49,22 +49,22 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
{/* Create event on selected calendar */}
|
{/* Create event on selected calendar */}
|
||||||
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
|
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
|
||||||
<p className="mt-4 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
|
<p className="text-subtle mt-4 text-sm">{t("connect_calendars_from_app_store")}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Connect calendars list */}
|
{/* Connect calendars list */}
|
||||||
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
|
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
|
||||||
<List className="mx-1 divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black sm:mx-0">
|
<List className="bg-default divide-subtle border-subtle mx-1 divide-y rounded-md border p-0 dark:bg-black sm:mx-0">
|
||||||
{queryIntegrations.data &&
|
{queryIntegrations.data &&
|
||||||
queryIntegrations.data.items.map((item) => (
|
queryIntegrations.data.items.map((item) => (
|
||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
{item.title && item.imageSrc && (
|
{item.title && item.logo && (
|
||||||
<CalendarItem
|
<CalendarItem
|
||||||
type={item.type}
|
type={item.type}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
imageSrc={item.imageSrc}
|
logo={item.logo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@@ -73,7 +73,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{queryIntegrations.isLoading && (
|
{queryIntegrations.isLoading && (
|
||||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
|
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
||||||
{[0, 0, 0, 0].map((_item, index) => {
|
{[0, 0, 0, 0].map((_item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
||||||
@@ -89,7 +89,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
type="button"
|
type="button"
|
||||||
data-testid="save-calendar-button"
|
data-testid="save-calendar-button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white",
|
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||||
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
|
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => nextStep()}
|
onClick={() => nextStep()}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
|
className="bg-default dark:text-inverted text-emphasis w-full dark:bg-opacity-5"
|
||||||
form={availabilityForm}
|
form={availabilityForm}
|
||||||
handleSubmit={async (values) => {
|
handleSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
|
className="border-default focus:ring-empthasis mt-1 block w-full rounded-sm border px-3 py-2 text-sm focus:border-gray-800 focus:outline-none"
|
||||||
defaultValue={imageSrc}
|
defaultValue={imageSrc}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center px-4">
|
<div className="flex items-center px-4">
|
||||||
@@ -142,19 +142,19 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset className="mt-8">
|
<fieldset className="mt-8">
|
||||||
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
|
<Label className="text-default mb-2 block text-sm font-medium">{t("about")}</Label>
|
||||||
<Editor
|
<Editor
|
||||||
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||||
setText={(value: string) => setValue("bio", turndown(value))}
|
setText={(value: string) => setValue("bio", turndown(value))}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white">
|
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
||||||
{t("finish")}
|
{t("finish")}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -22,23 +25,30 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const userSettingsSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||||
|
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm<z.infer<typeof userSettingsSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: user?.name || "",
|
name: user?.name || "",
|
||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(userSettingsSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
telemetry.event(telemetryEventTypes.onboardingStarted);
|
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
const defaultOptions = { required: true, maxLength: 255 };
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const onSuccess = async () => {
|
const onSuccess = async () => {
|
||||||
await utils.viewer.me.invalidate();
|
await utils.viewer.me.invalidate();
|
||||||
@@ -63,27 +73,29 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
|
||||||
{t("full_name")}
|
{t("full_name")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
{...register("name", defaultOptions)}
|
{...register("name", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className="w-full rounded-md border border-gray-300 text-sm"
|
className="border-default w-full rounded-md border text-sm"
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||||
{t("required")}
|
{errors.name.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Timezone select field */}
|
{/* Timezone select field */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="text-default block text-sm font-medium">
|
||||||
{t("timezone")}
|
{t("timezone")}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -94,7 +106,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-2 w-full rounded-md text-sm"
|
className="mt-2 w-full rounded-md text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="mt-3 flex flex-row font-sans text-xs leading-tight text-gray-500 dark:text-white">
|
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
|
||||||
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export default function WizardLayout({
|
|||||||
}, [router.asPath]);
|
}, [router.asPath]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black" data-testid="onboarding">
|
<div
|
||||||
|
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
|
||||||
|
data-testid="onboarding">
|
||||||
<div>
|
<div>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function WizardLayout({
|
|||||||
<div className="mx-auto sm:max-w-[520px]">
|
<div className="mx-auto sm:max-w-[520px]">
|
||||||
<header>
|
<header>
|
||||||
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title} </p>
|
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title} </p>
|
||||||
<p className="font-sans text-sm font-normal text-gray-500">{subtitle} </p>
|
<p className="text-subtle font-sans text-sm font-normal">{subtitle} </p>
|
||||||
</header>
|
</header>
|
||||||
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
|
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ const ChangePasswordSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||||
<div className="py-6 lg:pb-5">
|
<div className="py-6 lg:pb-5">
|
||||||
<div className="my-3">
|
<div className="my-3">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
|
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
|
||||||
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
|
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
|
||||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="current_password" className="text-default block text-sm font-medium">
|
||||||
{t("current_password")}
|
{t("current_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -77,13 +77,13 @@ const ChangePasswordSection = () => {
|
|||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password"
|
id="current_password"
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
placeholder={t("your_old_password")}
|
placeholder={t("your_old_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="new_password" className="text-default block text-sm font-medium">
|
||||||
{t("new_password")}
|
{t("new_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -94,7 +94,7 @@ const ChangePasswordSection = () => {
|
|||||||
value={newPassword}
|
value={newPassword}
|
||||||
required
|
required
|
||||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
placeholder={t("super_secure_new_password")}
|
placeholder={t("super_secure_new_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
labelProps={{
|
labelProps={{
|
||||||
className: "block text-sm font-medium text-gray-700",
|
className: "block text-sm font-medium text-default",
|
||||||
}}
|
}}
|
||||||
{...form.register("password")}
|
{...form.register("password")}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||||
/>
|
/>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
|||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||||
{t("user_impersonation_heading")}
|
{t("user_impersonation_heading")}
|
||||||
</h2>
|
</h2>
|
||||||
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
|
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
|
||||||
{!disableImpersonation ? t("enabled") : t("disabled")}
|
{!disableImpersonation ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">{t("user_impersonation_description")}</p>
|
<p className="text-subtle mt-1 text-sm">{t("user_impersonation_description")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-0 sm:self-center">
|
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<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">
|
||||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
<label htmlFor="password" className="text-default mt-4 block text-sm font-medium">
|
||||||
{t("password")}
|
{t("password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -146,7 +146,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,22 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
className="ms-2 me-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button
|
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
type="submit"
|
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
|
||||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
|||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
|
||||||
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
||||||
{enabled ? t("enabled") : t("disabled")}
|
{enabled ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
<p className="text-subtle mt-1 text-sm">{t("add_an_extra_layer_of_security")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-0 sm:self-center">
|
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { FiShield } from "@calcom/ui/components/icon";
|
import { Shield } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<FiShield className="h-6 w-6 text-white" />
|
<Shield className="text-inverted h-6 w-6" />
|
||||||
</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:ml-4 sm:text-left">
|
||||||
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-400">{description}</p>
|
<p className="text-muted text-sm">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,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 { Badge, showToast, Switch } from "@calcom/ui";
|
import { Badge, showToast, Switch } from "@calcom/ui";
|
||||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
import { ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
export function CalendarSwitch(props: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{props.defaultSelected && (
|
{props.defaultSelected && (
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
<div className="flex items-center">
|
||||||
|
<ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
labelProps={{
|
labelProps={{
|
||||||
className: "block text-sm font-medium text-gray-700",
|
className: "block text-sm font-medium text-default",
|
||||||
}}
|
}}
|
||||||
{...form.register("password")}
|
{...form.register("password")}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||||
/>
|
/>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -171,22 +171,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
className="ms-2 me-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button
|
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
type="submit"
|
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
|
||||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
|||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { FiUserCheck } from "@calcom/ui/components/icon";
|
import { UserCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
|
|||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
}}>
|
}}>
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiUserCheck}
|
Icon={UserCheck}
|
||||||
headline={t("admin_user_created")}
|
headline={t("admin_user_created")}
|
||||||
description={t("admin_user_created_description")}
|
description={t("admin_user_created_description")}
|
||||||
/>
|
/>
|
||||||
@@ -111,7 +111,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
|||||||
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
|
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
|
||||||
<span className="block">{t("username")}</span>
|
<span className="block">{t("username")}</span>
|
||||||
{longWebsiteUrl && (
|
{longWebsiteUrl && (
|
||||||
<small className="items-centerpx-3 mt-2 inline-flex rounded-t-md border border-b-0 border-gray-300 bg-gray-100 py-1 px-3 text-gray-500">
|
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 py-1 px-3">
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
@@ -119,7 +119,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
|||||||
<TextField
|
<TextField
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
!longWebsiteUrl && (
|
!longWebsiteUrl && (
|
||||||
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500">
|
<span className="text-subtle inline-flex items-center rounded-none px-3 text-sm">
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ const ChooseLicense = (
|
|||||||
<RadioGroup.Item value="FREE">
|
<RadioGroup.Item value="FREE">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
|
||||||
value === "FREE" && "ring-2 ring-black"
|
value === "FREE" && "ring-2 ring-black"
|
||||||
)}>
|
)}>
|
||||||
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
|
<h2 className="font-cal text-emphasis text-xl">{t("agplv3_license")}</h2>
|
||||||
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||||
<p className="text-gray-500">{t("forever_open_and_free")}</p>
|
<p className="text-subtle">{t("forever_open_and_free")}</p>
|
||||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
<ul className="text-subtle ml-4 list-disc text-left text-xs">
|
||||||
<li>{t("required_to_keep_your_code_open_source")}</li>
|
<li>{t("required_to_keep_your_code_open_source")}</li>
|
||||||
<li>{t("cannot_repackage_and_resell")}</li>
|
<li>{t("cannot_repackage_and_resell")}</li>
|
||||||
<li>{t("no_enterprise_features")}</li>
|
<li>{t("no_enterprise_features")}</li>
|
||||||
@@ -53,15 +53,15 @@ const ChooseLicense = (
|
|||||||
<RadioGroup.Item value="EE">
|
<RadioGroup.Item value="EE">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
|
||||||
value === "EE" && "ring-2 ring-black"
|
value === "EE" && "ring-2 ring-black"
|
||||||
)}>
|
)}>
|
||||||
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
|
<h2 className="font-cal text-emphasis text-xl">{t("ee_enterprise_license")}</h2>
|
||||||
<p className="font-medium text-green-800">
|
<p className="font-medium text-green-800">
|
||||||
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
|
<p className="text-subtle">{t("enterprise_license_includes")}</p>
|
||||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
<ul className="text-subtle ml-4 list-disc text-left text-xs">
|
||||||
<li>{t("no_need_to_keep_your_code_open_source")}</li>
|
<li>{t("no_need_to_keep_your_code_open_source")}</li>
|
||||||
<li>{t("repackage_rebrand_resell")}</li>
|
<li>{t("repackage_rebrand_resell")}</li>
|
||||||
<li>{t("a_vast_suite_of_enterprise_features")}</li>
|
<li>{t("a_vast_suite_of_enterprise_features")}</li>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TextField } from "@calcom/ui";
|
import { Button, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type EnterpriseLicenseFormValues = {
|
type EnterpriseLicenseFormValues = {
|
||||||
licenseKey: string;
|
licenseKey: string;
|
||||||
@@ -87,18 +87,18 @@ const EnterpriseLicense = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
|
<form {...rest} className="bg-default space-y-4 rounded-md px-8 py-10" onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-center text-lg"
|
className="w-full justify-center text-lg"
|
||||||
EndIcon={FiExternalLink}
|
EndIcon={ExternalLink}
|
||||||
href="https://console.cal.com"
|
href="https://console.cal.com"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
{t("purchase_license")}
|
{t("purchase_license")}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
|
<hr className="border-subtle my-8 w-full border-[1.5px]" />
|
||||||
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
|
<span className="bg-default absolute mt-[22px] px-3.5 text-sm">OR</span>
|
||||||
</div>
|
</div>
|
||||||
{t("already_have_key")}
|
{t("already_have_key")}
|
||||||
<Controller
|
<Controller
|
||||||
@@ -108,7 +108,7 @@ const EnterpriseLicense = (
|
|||||||
<TextField
|
<TextField
|
||||||
{...formMethods.register("licenseKey")}
|
{...formMethods.register("licenseKey")}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-0 group-hover:border-gray-400",
|
"group-hover:border-emphasis mb-0",
|
||||||
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||||
)}
|
)}
|
||||||
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
||||||
@@ -116,14 +116,14 @@ const EnterpriseLicense = (
|
|||||||
value={value}
|
value={value}
|
||||||
addOnFilled={false}
|
addOnFilled={false}
|
||||||
addOnClassname={classNames(
|
addOnClassname={classNames(
|
||||||
"hover:border-gray-300",
|
"hover:border-default",
|
||||||
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
|
errors.licenseKey === undefined && isDirty && "group-hover:border-emphasis"
|
||||||
)}
|
)}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
checkLicenseLoading ? (
|
checkLicenseLoading ? (
|
||||||
<FiLoader className="h-5 w-5 animate-spin" />
|
<Loader className="h-5 w-5 animate-spin" />
|
||||||
) : errors.licenseKey === undefined && isDirty ? (
|
) : errors.licenseKey === undefined && isDirty ? (
|
||||||
<FiCheck className="h-5 w-5 text-green-700" />
|
<Check className="h-5 w-5 text-green-700" />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
color={errors.licenseKey ? "warn" : ""}
|
color={errors.licenseKey ? "warn" : ""}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiCheck } from "@calcom/ui/components/icon";
|
import { Check } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const StepDone = (props: {
|
const StepDone = (props: {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
@@ -23,8 +23,8 @@ const StepDone = (props: {
|
|||||||
router.replace(props.nextStepPath);
|
router.replace(props.nextStepPath);
|
||||||
}}>
|
}}>
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
|
||||||
<FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[420px] text-center">
|
<div className="max-w-[420px] text-center">
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { Avatar } from "@calcom/ui";
|
|||||||
|
|
||||||
type TeamType = NonNullable<TeamWithMembers>;
|
type TeamType = NonNullable<TeamWithMembers>;
|
||||||
type MembersType = TeamType["members"];
|
type MembersType = TeamType["members"];
|
||||||
type MemberType = MembersType[number];
|
type MemberType = MembersType[number] & { safeBio: string | null };
|
||||||
|
|
||||||
|
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
|
||||||
|
|
||||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -17,19 +19,19 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={`/${member.username}`}>
|
<Link key={member.id} href={`/${member.username}`}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||||
/>
|
/>
|
||||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||||
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
|
<p className="text-default font-medium">{member.name}</p>
|
||||||
<div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
|
<div className="line-clamp-3 text-subtle overflow-ellipsis text-sm font-normal">
|
||||||
{!isBioEmpty ? (
|
{!isBioEmpty ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -43,7 +45,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
|
const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
|
||||||
if (!members || members.length === 0) {
|
if (!members || members.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Team = ({ team }: { team: TeamType }) => {
|
const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Members members={team.members} teamName={team.name} />
|
<Members members={team.members} teamName={team.name} />
|
||||||
|
|||||||
@@ -24,18 +24,18 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<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-gray-900">{props.heading}</h2>}
|
{props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
|
||||||
</div>
|
</div>
|
||||||
{props.loading && (
|
{props.loading && (
|
||||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
<div className="bg-muted absolute z-50 flex h-screen w-full items-center">
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
|
<div className="bg-default border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div>
|
<div className="text-default mt-8 text-center text-sm">{props.footerText}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import classNames from "classnames";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { ControllerRenderProps } from "react-hook-form";
|
import type { ControllerRenderProps } from "react-hook-form";
|
||||||
|
|
||||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
import { Edit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const EditableHeading = function EditableHeading({
|
const EditableHeading = function EditableHeading({
|
||||||
value,
|
value,
|
||||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
|
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
{...passThroughProps}
|
{...passThroughProps}
|
||||||
@@ -29,7 +29,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
value={value}
|
value={value}
|
||||||
required
|
required
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
|
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
||||||
)}
|
)}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiInfo } from "@calcom/ui/components/icon";
|
import { Info } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function InfoBadge({ content }: { content: string }) {
|
export default function InfoBadge({ content }: { content: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip side="top" content={content}>
|
<Tooltip side="top" content={content}>
|
||||||
<span title={content}>
|
<span title={content}>
|
||||||
<FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
|
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
className="text-md hover:bg-emphasis hover:text-emphasis text-default flex items-center rounded-sm px-2 py-1 text-sm font-medium">
|
||||||
<props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
<props.Icon className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function ModalContainer(
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
|
"bg-default inline-block w-full transform text-left align-bottom transition-all sm:align-middle",
|
||||||
{
|
{
|
||||||
"sm:w-full sm:max-w-lg ": !props.wide,
|
"sm:w-full sm:max-w-lg ": !props.wide,
|
||||||
"sm:w-4xl sm:max-w-4xl": props.wide,
|
"sm:w-4xl sm:max-w-4xl": props.wide,
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ const PoweredByCal = () => {
|
|||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
return (
|
return (
|
||||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
||||||
<Link
|
<Link href={POWERED_BY_URL} target="_blank" className="text-subtle opacity-50 hover:opacity-100">
|
||||||
href={POWERED_BY_URL}
|
|
||||||
target="_blank"
|
|
||||||
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
|
||||||
{t("powered_by")}{" "}
|
{t("powered_by")}{" "}
|
||||||
<img
|
<img
|
||||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
|
<label htmlFor={htmlFor} className="text-default mt-1 flex text-sm font-medium">
|
||||||
<Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
<Icon className="text-subtle mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
|||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
@@ -195,7 +195,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isInputUsernamePremium ? "border border-orange-400 " : "",
|
isInputUsernamePremium ? "border border-orange-400 " : "",
|
||||||
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
|
"border-default bg-muted text-subtle hidden h-9 items-center rounded-l-md border border-r-0 border-r-gray-300 px-3 text-sm md:inline-flex"
|
||||||
)}>
|
)}>
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
||||||
</span>
|
</span>
|
||||||
@@ -216,7 +216,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
markAsError
|
markAsError
|
||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
||||||
: "border-l-gray-300",
|
: "border-l-gray-300",
|
||||||
disabled ? "bg-gray-100 text-gray-400 focus:border-0" : ""
|
disabled ? "bg-subtle text-muted focus:border-0" : ""
|
||||||
)}
|
)}
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
usernameIsAvailable ? "" : ""
|
usernameIsAvailable ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||||
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
|
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,29 +250,30 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{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>}
|
||||||
|
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent>
|
<DialogContent
|
||||||
<div className="flex flex-row">
|
Icon={Edit2}
|
||||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
title={t("confirm_username_change_dialog_title")}
|
||||||
<FiEdit2 className="m-auto h-6 w-6" />
|
description={
|
||||||
</div>
|
<>
|
||||||
<div className="mb-4 w-full px-4 pt-1">
|
|
||||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
|
||||||
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
|
<p className="text-default mb-4 text-sm">{t("change_username_standard_to_premium")}</p>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
}>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="mb-4 w-full px-4 pt-1">
|
||||||
|
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
||||||
<div className="flex-1 px-2">
|
<div className="flex-1 px-2">
|
||||||
<p className="text-gray-500">{t("current_username")}</p>
|
<p className="text-subtle">{t("current_username")}</p>
|
||||||
<p className="mt-1" data-testid="current-username">
|
<p className="text-emphasis mt-1" data-testid="current-username">
|
||||||
{currentUsername}
|
{currentUsername}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-6 flex-1">
|
<div className="ml-6 flex-1">
|
||||||
<p className="text-gray-500" data-testid="new-username">
|
<p className="text-subtle" data-testid="new-username">
|
||||||
{t("new_username")}
|
{t("new_username")}
|
||||||
</p>
|
</p>
|
||||||
<p>{inputUsernameValue}</p>
|
<p className="text-emphasis">{inputUsernameValue}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
data-testid="go-to-billing"
|
data-testid="go-to-billing"
|
||||||
href={paymentLink}>
|
href={paymentLink}>
|
||||||
<>
|
<>
|
||||||
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
|
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
import { Check, Edit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
currentUsername: string | undefined;
|
currentUsername: string | undefined;
|
||||||
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
|
"mb-0 mt-0 rounded-md rounded-l-none",
|
||||||
markAsError
|
markAsError
|
||||||
? "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"
|
||||||
: ""
|
: ""
|
||||||
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||||
<span className={classNames("mx-2 py-2")}>
|
<span className={classNames("mx-2 py-2")}>
|
||||||
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
|
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -154,31 +154,25 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent>
|
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
|
||||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
<div className="flex flex-row">
|
||||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
<div className="mb-4 w-full pt-1">
|
||||||
<FiEdit2 className="m-auto h-6 w-6" />
|
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
||||||
</div>
|
|
||||||
<div className="mb-4 w-full px-4 pt-1">
|
|
||||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
|
||||||
<div className="flex-1 px-2">
|
<div className="flex-1 px-2">
|
||||||
<p className="text-gray-500">{t("current_username")}</p>
|
<p className="text-subtle">{t("current_username")}</p>
|
||||||
<p className="mt-1" data-testid="current-username">
|
<p className="text-emphasis mt-1" data-testid="current-username">
|
||||||
{currentUsername}
|
{currentUsername}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="ml-6 flex-1">
|
||||||
<p className="text-gray-500" data-testid="new-username">
|
<p className="text-subtle" data-testid="new-username">
|
||||||
{t("new_username")}
|
{t("new_username")}
|
||||||
</p>
|
</p>
|
||||||
<p>{inputUsernameValue}</p>
|
<p className="text-emphasis">{inputUsernameValue}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{React.createElement(
|
{React.createElement(
|
||||||
descriptionAsLabel ? "div" : "label",
|
descriptionAsLabel ? "div" : "label",
|
||||||
{
|
{
|
||||||
className: "flex text-sm font-medium text-gray-700",
|
className: "flex text-sm font-medium text-default",
|
||||||
...(!descriptionAsLabel
|
...(!descriptionAsLabel
|
||||||
? {
|
? {
|
||||||
htmlFor: rest.id,
|
htmlFor: rest.id,
|
||||||
@@ -40,7 +40,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{
|
{
|
||||||
className: classNames(
|
className: classNames(
|
||||||
"relative flex items-start",
|
"relative flex items-start",
|
||||||
descriptionAsLabel ? "text-gray-700" : "text-gray-900"
|
descriptionAsLabel ? "text-default" : "text-emphasis"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
<>
|
<>
|
||||||
@@ -49,10 +49,10 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
className="text-primary-600 focus:ring-primary-500 border-default h-4 w-4 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
|
<span className="ms-3 text-sm">{description}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{informationIconText && <InfoBadge content={informationIconText} />}
|
{informationIconText && <InfoBadge content={informationIconText} />}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@ export const CheckedSelect = ({
|
|||||||
alt={option.label}
|
alt={option.label}
|
||||||
/>
|
/>
|
||||||
{option.label}
|
{option.label}
|
||||||
<FiX
|
<X
|
||||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||||
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
|
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loaded 100 of 877 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user