Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56a2b66190 | ||
|
|
a0f55707e2 | ||
|
|
b247075a19 | ||
|
|
25ec5313fb | ||
|
|
33f35e77fd | ||
|
|
69808bb9a4 | ||
|
|
18b6a93476 | ||
|
|
e627cc51fd | ||
|
|
b349def58b | ||
|
|
6a6faf30ff | ||
|
|
5616dba4db | ||
|
|
bca1a1b86c | ||
|
|
ddf7a234ab | ||
|
|
fa7260d72a | ||
|
|
5f9f6ef025 | ||
|
|
3d41c64d81 | ||
|
|
8a9b985760 | ||
|
|
c8956680ad | ||
|
|
c2668acd33 | ||
|
|
89bf8321ba | ||
|
|
da625f1527 | ||
|
|
2dddd4ce77 | ||
|
|
cc1d606ba8 | ||
|
|
262c8cf37c | ||
|
|
4366405b21 | ||
|
|
07399974ce | ||
|
|
9d7cd73c7f | ||
|
|
1551a293c5 | ||
|
|
47e948fbbc | ||
|
|
c8b01f6992 | ||
|
|
f3143d99ee | ||
|
|
61a5c3a3b5 | ||
|
|
056edd8ec2 | ||
|
|
d8387835a8 | ||
|
|
26ea249481 | ||
|
|
2d26d73eb3 | ||
|
|
b8b24c04dc | ||
|
|
62dcd6612c | ||
|
|
295748adca | ||
|
|
6e93224c9b | ||
|
|
4d8198d113 | ||
|
|
5fe0ca7913 | ||
|
|
cfb625e934 | ||
|
|
e12b21a73c | ||
|
|
841346c5f2 | ||
|
|
1270742078 | ||
|
|
517cfde5b8 | ||
|
|
51bf613621 | ||
|
|
f4c3ab6431 | ||
|
|
e8acc3e500 | ||
|
|
a6ba0f5b57 | ||
|
|
b82d924f03 | ||
|
|
1cdb51f424 | ||
|
|
aca0dc1b01 | ||
|
|
e5e8c33c34 | ||
|
|
c76847a8f1 | ||
|
|
7b1ced914b | ||
|
|
a6cd173910 | ||
|
|
dcd39c3f04 | ||
|
|
aa0a1fcc95 | ||
|
|
76cf352bce | ||
|
|
86ab19be30 | ||
|
|
e4b766b9d6 | ||
|
|
2529f71770 | ||
|
|
9ea0f02c93 | ||
|
|
fb1828c8f9 | ||
|
|
12f2c1f431 | ||
|
|
29e9bd0ee7 | ||
|
|
9c794bac1a | ||
|
|
feca7c23fb | ||
|
|
31599cdd26 | ||
|
|
e018eb9ebe | ||
|
|
a3f3f1f263 | ||
|
|
d19eaae5b6 | ||
|
|
4eecd42204 | ||
|
|
b9f90144c5 | ||
|
|
8b07b839e9 | ||
|
|
2a98ca4b91 | ||
|
|
b9064b1f72 | ||
|
|
0ec71e52ef | ||
|
|
c20835a4c8 | ||
|
|
8c63b42cec | ||
|
|
ddb752102f | ||
|
|
ef0a7950cf | ||
|
|
641301afab | ||
|
|
7d1bc5f0c5 | ||
|
|
aaccfa49ce | ||
|
|
1b1f3ddfd9 | ||
|
|
f7eda5aa3a | ||
|
|
b2b846647d | ||
|
|
4edc973876 | ||
|
|
3b0126ff35 | ||
|
|
cc4192817b | ||
|
|
a8a10c0f35 | ||
|
|
5f7d904349 | ||
|
|
abce20824f | ||
|
|
76a8c89d33 | ||
|
|
fc9360d99a | ||
|
|
a5c21d5e3e | ||
|
|
3960c8a954 | ||
|
|
0cbcda7430 | ||
|
|
ff12e35748 | ||
|
|
15d967b1d2 | ||
|
|
b309be1d94 | ||
|
|
94dc2643f2 | ||
|
|
6440d3da6a | ||
|
|
9e08cf88f4 | ||
|
|
8b3eb986df | ||
|
|
8a6a7524cc | ||
|
|
1dbdb8a4d4 | ||
|
|
4fb0852505 | ||
|
|
0815ab715b | ||
|
|
944ff68463 | ||
|
|
676254e96b | ||
|
|
207bcb9a97 | ||
|
|
4d825672db | ||
|
|
ba86e7f540 | ||
|
|
3206e4ea12 | ||
|
|
dbcebec31b | ||
|
|
f427c09e88 | ||
|
|
ded838881f | ||
|
|
08da9d805a | ||
|
|
7cd0d5c3c3 | ||
|
|
b018ee34a7 | ||
|
|
17345f9326 | ||
|
|
a1d7b30d51 | ||
|
|
87dbfba5df | ||
|
|
e5d44d029a | ||
|
|
4803817055 | ||
|
|
dac6bff5b1 | ||
|
|
a3c10d0beb | ||
|
|
d2b45e52ff | ||
|
|
0da6878188 | ||
|
|
572b54b7f0 | ||
|
|
d9bf89beab | ||
|
|
0d1f484c92 | ||
|
|
51d02c9873 | ||
|
|
6226baa244 | ||
|
|
81d842ade7 | ||
|
|
d26732c9dc | ||
|
|
f5318c6be3 | ||
|
|
dafbebe6f3 | ||
|
|
4adfcb4b6b | ||
|
|
3ac6a8aa39 | ||
|
|
c8019edb57 | ||
|
|
bfc81b770a | ||
|
|
4a5404bf21 | ||
|
|
7a9b40f218 | ||
|
|
dca1d35cb4 | ||
|
|
af970c049a | ||
|
|
6fc3840ca3 | ||
|
|
c51150350a | ||
|
|
51664bf733 | ||
|
|
831506447a | ||
|
|
ee54e670cb | ||
|
|
99cd6fe7da | ||
|
|
75c7e03efe | ||
|
|
2ebfcf448c | ||
|
|
235ec0e906 | ||
|
|
ea73870ce9 | ||
|
|
d1440bb5d2 | ||
|
|
a000030040 | ||
|
|
44864a801d | ||
|
|
b9fd0b4c78 | ||
|
|
0e7276370f | ||
|
|
4d2cf28c7e | ||
|
|
55ee939cf9 | ||
|
|
2fe54ded15 | ||
|
|
8e3fa4be29 | ||
|
|
d7b4d5932d | ||
|
|
3c7fc85b34 | ||
|
|
af018f0e8b | ||
|
|
6573f75a83 | ||
|
|
a3b26cd1e7 | ||
|
|
4f99746f68 | ||
|
|
fd76bc9cab | ||
|
|
602c813928 | ||
|
|
2c54ea90f9 | ||
|
+17 |
2698cde311 | ||
|
|
1433ec6946 | ||
|
|
48e5d18ea1 | ||
|
|
5da76c698e | ||
|
|
578308a8cb | ||
|
|
6b3862f96d | ||
|
|
94f239bc42 | ||
|
|
2f2f6006e6 | ||
|
|
f8558e16e8 | ||
|
|
38be2bef9a | ||
|
|
485619602f | ||
|
|
ce01537fe6 | ||
|
|
f77e7476d2 | ||
|
|
b6016c8f20 | ||
|
|
a5e5d103a0 | ||
|
|
fec02979ba | ||
|
|
dc200b95cb | ||
|
|
78a54e5f54 | ||
|
|
00ebda2fc6 | ||
|
|
7c749299bb | ||
|
|
3df289b557 | ||
|
|
f4c849b75c | ||
|
|
09cc6f07e4 | ||
|
|
78656f2116 | ||
|
|
1fa5e296af | ||
|
|
0f76530c11 | ||
|
|
ab6758194b | ||
|
|
5528d60897 | ||
|
|
bb1c55e07f | ||
|
|
c899921591 | ||
|
|
ea34545d72 | ||
|
|
9d3540511b | ||
|
|
e8cf92361a | ||
|
|
e8525b963b | ||
|
|
24dabd0ddc | ||
|
|
5203168b2e | ||
|
|
c3e40f84a9 | ||
|
|
8e2ece7526 | ||
|
|
b6eca6e083 | ||
|
|
4cb264f1f7 | ||
|
|
e248fda36b | ||
|
|
1c97e549eb | ||
|
|
77600f8168 | ||
|
|
6cad606ba1 | ||
|
|
7cae7a7bd4 | ||
|
|
93f65807e7 | ||
|
|
0a63820699 | ||
|
|
05ad437b27 | ||
|
|
26a57a71ae | ||
|
|
34a10205a3 | ||
|
|
fbf8ff6db7 | ||
|
|
bef6d18fdf | ||
|
|
2e96e2ef3f | ||
|
|
2ca6769a70 | ||
|
|
1eb514dfcf | ||
|
|
d6c004ba22 | ||
|
+17 |
afb9605f0a | ||
|
|
a2549bd7b6 | ||
|
|
c2a8f799ff | ||
|
|
c68ff54e77 | ||
|
|
74fe20802d | ||
|
|
d59cda3286 | ||
|
|
1cae2b6f3d | ||
|
|
9d9fa8e575 | ||
|
|
f04c634c48 | ||
|
|
c3e7981917 | ||
|
|
d6594dab0f | ||
|
|
5fcfbb2d90 | ||
|
|
8a99c3d2a5 | ||
|
|
336c2d3643 | ||
|
|
cfe3ef4ab0 | ||
|
|
4ec8241a86 | ||
|
|
e6313aafac | ||
|
|
f87f030bb5 | ||
|
|
f87254cdc3 | ||
|
|
1f74837189 | ||
|
|
c7064906c9 | ||
|
|
86c94a3cd5 | ||
|
|
2056ebe186 | ||
|
|
53424ca9ae | ||
|
|
8197ba18d4 | ||
|
|
d341ea5111 | ||
|
|
42dcd4a2a9 | ||
|
|
c63225fdbf | ||
|
|
41774e4932 | ||
|
|
af7849f951 | ||
|
|
5353610d9a | ||
|
+14 |
156f3e5de2 | ||
|
|
05f51459a6 | ||
|
|
f0db97bbb1 | ||
|
|
05c33bd94b | ||
|
|
7d4cc59cd2 | ||
|
|
a94465ff04 | ||
|
|
4d54ac3ca2 | ||
|
|
15da690270 | ||
|
|
2462e8e597 | ||
|
|
2f628a17df | ||
|
|
1ee3783db3 | ||
|
|
b3d8d7e944 | ||
|
|
a4b7f1af65 | ||
|
|
f7445bf8a1 | ||
|
|
9cad7086d7 | ||
|
|
3345ec61ee | ||
|
|
7797421ce9 | ||
|
|
075a274207 | ||
|
|
a46a31040a | ||
|
|
7a28e1d6f4 | ||
|
|
51fd7fdb3e | ||
|
|
d01963303a | ||
|
|
6877d0f693 | ||
|
|
39040fca9a | ||
|
|
6fa4befc9b | ||
|
|
8c9871c6ce | ||
|
+8 |
a9af2fb255 | ||
|
|
8c150264f4 | ||
|
|
cfc3644d51 | ||
|
|
bdb4a7353b | ||
|
|
30c0e6d1d7 | ||
|
|
390967990a | ||
|
|
cc56602474 | ||
|
|
43a044d59e | ||
|
|
5b8ed9c3ef | ||
|
|
c3d6df2dcf | ||
|
|
9d02cb493c | ||
|
|
26ed478e50 | ||
|
|
09e338970d | ||
|
|
f60399ca8b | ||
|
|
5cf7e32852 | ||
|
|
24be8a63f2 | ||
|
|
75509c97fd | ||
|
|
9b26c7aee6 | ||
|
|
f18e80cfcf | ||
|
|
0a4b1ca014 | ||
|
|
9fa6aced33 | ||
|
|
9093909075 | ||
|
|
832fce91e0 | ||
|
|
6da41fce63 | ||
|
|
3b51cc9aef | ||
|
|
50c8959cfa | ||
|
|
c96d258f5e | ||
|
|
6e45b1c383 | ||
|
|
551ebe863b | ||
|
|
fa7b94bfe9 | ||
|
|
56c5d39cd3 | ||
|
|
7574fb6f44 | ||
|
|
901d1f0e01 | ||
|
|
828fcbe1d9 | ||
|
|
34db46869e | ||
|
|
1f1cab2be1 | ||
|
|
6cf53feaf8 | ||
|
|
376b693d91 | ||
|
|
831aa72d23 | ||
|
|
ef78db7856 | ||
|
|
7ee5967075 | ||
|
|
5b165e6ebf | ||
|
|
624f52c305 | ||
|
|
df25ae6a3d | ||
|
|
7f65d7f3c3 | ||
|
|
c42674595c | ||
|
|
17eb45b0b6 | ||
|
|
48d0edeaf4 | ||
|
|
ed4b31767f | ||
|
|
cebb10bfe4 | ||
|
|
7a6e7c2f1d | ||
|
|
0ffed24359 | ||
|
|
e121615d36 | ||
|
|
4f9aa8bd96 | ||
|
|
64ede37ad6 | ||
|
|
36b6c351ec | ||
|
|
9ab5a3149c | ||
|
|
3b8f436bdf | ||
|
|
77d45a6ec2 | ||
|
|
50c74cf286 | ||
|
|
dcbafced8b | ||
|
|
4679c80ea0 | ||
|
|
291316ee01 | ||
|
|
208fc2f0e4 | ||
|
|
99e71054ff | ||
|
|
9d118d4e34 | ||
|
+6 |
d1a877e70d | ||
|
|
238b509be6 | ||
|
|
c7471b404c | ||
|
|
4db4df8ace | ||
|
|
a15b8fb92e | ||
|
|
a4bed48eaf | ||
|
|
1be6f192d4 | ||
|
|
f1c5838aef | ||
|
|
8e994dcc33 | ||
|
|
506bbe3d31 | ||
|
|
419a84c5a2 | ||
|
|
8cf0050d1f | ||
|
|
6c23813192 | ||
|
|
875f922597 | ||
|
|
c8e3f6494a | ||
|
|
333801d2ae | ||
|
|
60f7528cbc | ||
|
|
0faf294367 | ||
|
|
87c178e53b | ||
|
|
7710b1b718 | ||
|
|
d71c6ccb31 | ||
|
|
33a4117fb5 | ||
|
|
a90419c40b | ||
|
|
16adf86c8c | ||
|
|
3b838c475a | ||
|
|
cf33106d8b | ||
|
|
a63f9ddf61 | ||
|
|
1cf83068b2 | ||
|
|
23de92f818 | ||
|
|
c6fc1f756b | ||
|
|
88b7c506f1 | ||
|
|
755aae969c | ||
|
|
2d50d09ce6 | ||
|
|
a22397dbb4 | ||
|
|
d1076982a7 | ||
|
|
d77e4f3c4a | ||
|
|
b1c0ea4c38 | ||
|
|
425e18f53c | ||
|
|
3611807838 | ||
|
|
5b1cd9e5ce | ||
|
|
00bff598ef | ||
|
|
3a8aa26c78 | ||
|
|
3c761f4451 | ||
|
|
8640eb9f52 | ||
|
|
abb5fd36f3 | ||
|
|
865663646a | ||
|
|
b7c92063c2 | ||
|
|
4f293edb6b | ||
|
|
0c88cf75e0 | ||
|
|
0b93647209 | ||
|
|
b931b00714 | ||
|
|
796a5f8d51 | ||
|
|
905a110377 | ||
|
|
6ccc19985f | ||
|
|
8f71474303 | ||
|
|
5f351230ff | ||
|
|
3e005919c1 | ||
|
|
cfa8fd8b67 | ||
|
|
a888be6e04 | ||
|
|
6aa2cba3ce | ||
|
|
12ab615684 | ||
|
|
1d792a2c6c | ||
|
|
9602ac29fb | ||
|
|
5714471b9a | ||
|
|
a99e93a4bd | ||
|
|
7f044ff92f |
No files matched your search
+11
-4
@@ -1,20 +1,21 @@
|
|||||||
# ********** INDEX **********
|
# ********** INDEX **********
|
||||||
#
|
#
|
||||||
# - LICENSE
|
# - LICENSE (DEPRECATED)
|
||||||
# - DATABASE
|
# - DATABASE
|
||||||
# - SHARED
|
# - SHARED
|
||||||
# - NEXTAUTH
|
# - NEXTAUTH
|
||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||||
#
|
#
|
||||||
# To enable enterprise-only features, fill your license key in here.
|
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
|
||||||
|
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
|||||||
# Set this to true in to disable new signups
|
# Set this to true in to disable new signups
|
||||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
CSP_POLICY=
|
||||||
|
|
||||||
|
# Vercel Edge Config
|
||||||
|
EDGE_CONFIG=
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This is a comment.
|
||||||
|
# Each line is a file pattern followed by one or more owners.
|
||||||
|
|
||||||
|
# These owners will be the default owners for everything in
|
||||||
|
# the repo. Unless a later match takes precedence,
|
||||||
|
# @global-owner1 and @global-owner2 will be requested for
|
||||||
|
# review when someone opens a pull request.
|
||||||
|
* @calcom/reviewers
|
||||||
|
|
||||||
|
.github/ @calcom/core
|
||||||
|
.husky/ @calcom/core
|
||||||
|
.vcode/ @calcom/core
|
||||||
|
.snaplet/ @calcom/core
|
||||||
|
deploy/ @calcom/core
|
||||||
|
scripts/ @calcom/core
|
||||||
|
|
||||||
|
apps/docs @calcom/docs
|
||||||
|
apps/swagger @calcom/docs
|
||||||
|
|
||||||
|
packages/app-store-cli @hariombalhara @calcom/core
|
||||||
|
packages/dayjs @zomars @calcom/core
|
||||||
|
packages/emails @zomars @calcom/core
|
||||||
|
packages/embeds @hariombalhara @calcom/core
|
||||||
|
packages/eslint-plugin @zomars @calcom/core
|
||||||
|
packages/features @zomars @calcom/core
|
||||||
|
packages/features/kbar @alishaz-polymath @calcom/core
|
||||||
|
packages/features/ee/impersonation @sean-brydon @calcom/core
|
||||||
|
packages/features/ee/payments @alannnc @calcom/core
|
||||||
|
packages/features/ee/workflows @CarinaWolli @calcom/core
|
||||||
|
packages/features/tips @PeerRich @calcom/core
|
||||||
|
packages/ui @calcom/ui
|
||||||
|
packages/config @calcom/core
|
||||||
|
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Cache production build binaries
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
inputs:
|
||||||
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache production build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
|
env:
|
||||||
|
cache-name: prod-build
|
||||||
|
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||||
|
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||||
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
# Ensures production-build.yml will always be fresh
|
||||||
|
key-4: ${{ github.sha }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ github.workspace }}/apps/web/.next
|
||||||
|
**/.turbo/**
|
||||||
|
**/dist/**
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
|
- run: yarn build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Cache database between jobs
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
inputs:
|
||||||
|
DATABASE_URL:
|
||||||
|
required: false
|
||||||
|
default: "postgresql://postgres:@localhost:5432/calendso"
|
||||||
|
path:
|
||||||
|
required: false
|
||||||
|
default: "backups/backup.sql"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache database
|
||||||
|
id: cache-db
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-db
|
||||||
|
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||||
|
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
with:
|
||||||
|
path: ${{ inputs.path }}
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||||
|
- run: yarn db-seed
|
||||||
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
- name: Postgres Dump Backup
|
||||||
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
|
uses: tj-actions/pg-dump@v2.3
|
||||||
|
with:
|
||||||
|
database_url: ${{ inputs.DATABASE_URL }}
|
||||||
|
path: ${{ inputs.path }}
|
||||||
|
options: "-O"
|
||||||
|
- name: Postgres Backup Restore
|
||||||
|
if: steps.cache-db.outputs.cache-hit == 'true'
|
||||||
|
uses: tj-actions/pg-restore@v4.5
|
||||||
|
with:
|
||||||
|
database_url: ${{ inputs.DATABASE_URL }}
|
||||||
|
backup_file: ${{ inputs.path }}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: Dangerous git Checkout
|
||||||
|
description: "Git Checkout from PR code so we can run checks from forks"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 2
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: "Set environment variables"
|
||||||
|
description: "Configures environment variables for a workflow"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Create env file
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: env-cache
|
||||||
|
with:
|
||||||
|
path: gh.env
|
||||||
|
key: env-cache-${{ hashFiles('gh.env') }}
|
||||||
|
restore-keys: env-cache-
|
||||||
|
- name: Set Environment Variables
|
||||||
|
uses: tw3lveparsecs/github-actions-setvars@latest
|
||||||
|
with:
|
||||||
|
envFilePath: gh.env
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Yarn install
|
||||||
|
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||||
|
inputs:
|
||||||
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Use Node ${{ inputs.node_version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node_version }}
|
||||||
|
cache: "yarn"
|
||||||
|
- name: Yarn install
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
yarn install --prefer-offline --frozen-lockfile
|
||||||
|
yarn prisma generate
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Install playwright binaries
|
||||||
|
description: "Install playwright, cache and restore if necessary"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
id: playwright-cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Caches/ms-playwright
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
${{ github.workspace }}/node_modules/playwright
|
||||||
|
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: cache-playwright-
|
||||||
|
- name: Yarn playwright install
|
||||||
|
shell: bash
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn playwright install
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: cleanup caches by a branch
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
gh extension install actions/gh-actions-cache
|
||||||
|
|
||||||
|
REPO=${{ github.repository }}
|
||||||
|
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||||
|
|
||||||
|
echo "Fetching list of cache key"
|
||||||
|
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||||
|
|
||||||
|
## Setting this to not fail the workflow while deleting cache keys.
|
||||||
|
set +e
|
||||||
|
echo "Deleting caches..."
|
||||||
|
for cacheKey in $cacheKeysForPR
|
||||||
|
do
|
||||||
|
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||||
|
done
|
||||||
|
echo "Done"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -1,34 +1,17 @@
|
|||||||
name: Check types
|
name: Check types
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**.tsx?"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
jobs:
|
jobs:
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
- uses: ./.github/actions/yarn-install
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Use Node 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- run: yarn type-check
|
- run: yarn type-check
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Cron - mark stale for inactive issues
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
# "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
|
||||||
|
schedule:
|
||||||
|
# Runs every day (see https://crontab.guru)
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v7
|
||||||
|
with:
|
||||||
|
days-before-stale: 60
|
||||||
|
include-only-assigned: true
|
||||||
|
days-before-close: -1
|
||||||
|
|
||||||
|
|
||||||
@@ -9,14 +9,12 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
required-checks:
|
|
||||||
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
|
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@1.4.9
|
uses: crowdin/github-action@1.4.9
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
db-staging-snapshot:
|
db-staging-snapshot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install Snaplet CLI
|
- name: Install Snaplet CLI
|
||||||
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
||||||
- name: Restore Snapshot
|
- name: Restore Snapshot
|
||||||
|
|||||||
@@ -1,60 +1,12 @@
|
|||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- apps/api/**
|
|
||||||
- apps/console/**
|
|
||||||
- apps/docs/**
|
|
||||||
- apps/swagger/**
|
|
||||||
- apps/website/**
|
|
||||||
- apps/web/public/**
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
matrix:
|
|
||||||
node: ["16.x"]
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -65,35 +17,18 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node ${{ matrix.node }}
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: ${{ matrix.node }}
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test-e2e:app-store
|
run: yarn test-e2e:app-store
|
||||||
|
env:
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
|||||||
@@ -1,56 +1,11 @@
|
|||||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/embeds/**
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -61,45 +16,28 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: 16.x
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
||||||
|
env:
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload embed-core results
|
- name: Upload embed-core results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-core
|
name: test-results-embed-core
|
||||||
path: packages/embeds/embed-core/playwright/results
|
path: packages/embeds/embed-core/playwright/results
|
||||||
|
|
||||||
- name: Upload embed-react results
|
- name: Upload embed-react results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-react
|
name: test-results-embed-react
|
||||||
path: packages/embeds/embed-react/playwright/results
|
path: packages/embeds/embed-react/playwright/results
|
||||||
+13
-78
@@ -1,59 +1,11 @@
|
|||||||
name: E2E test
|
name: E2E test
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/**
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
name: E2E tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -62,37 +14,20 @@ jobs:
|
|||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: 16.x
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
- name: Run Tests
|
||||||
- name: Cache playwright binaries
|
run: yarn test-e2e
|
||||||
uses: actions/cache@v2
|
env:
|
||||||
id: playwright-cache
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
with:
|
- name: Upload Test Results
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- run: yarn test-e2e
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Create .env file
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
env:
|
||||||
|
INPUT_ENV_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||||
|
INPUT_ENV_NEXTAUTH_SECRET: secret
|
||||||
|
INPUT_ENV_GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
|
INPUT_ENV_GOOGLE_LOGIN_ENABLED: true
|
||||||
|
# INPUT_ENV_CRON_API_KEY: xxx
|
||||||
|
INPUT_ENV_CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
INPUT_ENV_STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
INPUT_ENV_STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
INPUT_ENV_STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
INPUT_ENV_PAYMENT_FEE_PERCENTAGE: 0.005
|
||||||
|
INPUT_ENV_PAYMENT_FEE_FIXED: 10
|
||||||
|
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
INPUT_ENV_SAML_ADMINS: pro@example.com
|
||||||
|
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
|
||||||
|
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||||
|
# INPUT_ENV_MS_GRAPH_CLIENT_ID: xxx
|
||||||
|
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
|
||||||
|
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
|
||||||
|
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
|
||||||
|
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create_env_file:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ozaytsev86/create-env-file@v1
|
||||||
|
with:
|
||||||
|
file-name: ${{ github.workspace }}/gh.env
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: env-cache
|
||||||
|
with:
|
||||||
|
path: gh.env
|
||||||
|
key: env-cache-${{ hashFiles('gh.env') }}
|
||||||
@@ -1,38 +1,14 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**.tsx?"
|
|
||||||
- "**.jsx?"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
- uses: ./.github/actions/yarn-install
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Use Node 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Test Code Linting
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
- name: Save Code Linting Reports
|
- name: Save Code Linting Reports
|
||||||
run: yarn lint:report
|
run: yarn lint:report
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
name: "Next.js Bundle Analysis"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
analyze:
|
||||||
|
needs: build
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/cache-build
|
||||||
|
- name: Analyze bundle
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
npx -p nextjs-bundle-analysis report
|
||||||
|
|
||||||
|
- name: Upload bundle
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: bundle
|
||||||
|
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||||
|
|
||||||
|
- name: Download base branch bundle stats
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
if: success() && github.event.number
|
||||||
|
with:
|
||||||
|
workflow: nextjs-bundle-analysis.yml
|
||||||
|
branch: ${{ github.event.pull_request.base.ref }}
|
||||||
|
path: apps/web/.next/analyze/base
|
||||||
|
|
||||||
|
# And here's the second place - this runs after we have both the current and
|
||||||
|
# base branch bundle stats, and will compare them to determine what changed.
|
||||||
|
# There are two configurable arguments that come from package.json:
|
||||||
|
#
|
||||||
|
# - budget: optional, set a budget (bytes) against which size changes are measured
|
||||||
|
# it's set to 350kb here by default, as informed by the following piece:
|
||||||
|
# https://infrequently.org/2021/03/the-performance-inequality-gap/
|
||||||
|
#
|
||||||
|
# - red-status-percentage: sets the percent size increase where you get a red
|
||||||
|
# status indicator, defaults to 20%
|
||||||
|
#
|
||||||
|
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||||
|
# entry in your package.json file.
|
||||||
|
- name: Compare with base branch bundle
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||||
|
|
||||||
|
- name: Get comment body
|
||||||
|
id: get-comment-body
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
||||||
|
body="${body//'%'/'%25'}"
|
||||||
|
body="${body//$'\n'/'%0A'}"
|
||||||
|
body="${body//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=body::$body
|
||||||
|
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v1
|
||||||
|
if: success() && github.event.number
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
||||||
|
|
||||||
|
- name: Create Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
|
||||||
|
- name: Update Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
name: PR Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- ".github/CODEOWNERS"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
outputs:
|
||||||
|
app-store: ${{ steps.filter.outputs.app-store }}
|
||||||
|
embed: ${{ steps.filter.outputs.embed }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
app-store:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/app-store/**'
|
||||||
|
embed:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/embeds/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
name: Create env file
|
||||||
|
uses: ./.github/workflows/env-create-file.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
|
uses: ./.github/workflows/check-types.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Unit tests
|
||||||
|
uses: ./.github/workflows/test.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Linters
|
||||||
|
uses: ./.github/workflows/lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
needs: env
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: E2E tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-app-store:
|
||||||
|
name: E2E App Store tests
|
||||||
|
if: ${{ needs.changes.outputs.app-store == 'true' }}
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed:
|
||||||
|
name: E2E embeds tests
|
||||||
|
if: ${{ needs.changes.outputs.embed == 'true' }}
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
analyze:
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
required:
|
||||||
|
needs: [lint, type-check, test, build, e2e]
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: fail if conditional jobs failed
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: exit 1
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.1
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: calendso
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/env-read-file
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
|
- uses: ./.github/actions/cache-build
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
name: "Meta Workflow: Require Conditional Status Checks"
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_call:
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
required-checks:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Ensure All Conditional Checks Have Passed
|
|
||||||
uses: blend/require-conditional-status-checks@2022.02.04
|
|
||||||
with:
|
|
||||||
timeout: 15m
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
interval: 20s
|
|
||||||
# Question marks aren't working
|
|
||||||
# @see https://github.com/blend/require-conditional-status-checks/pull/11
|
|
||||||
checks-yaml: |
|
|
||||||
- job: lint
|
|
||||||
paths:
|
|
||||||
- /**/*.jsx
|
|
||||||
- /**/*.js
|
|
||||||
- /**/*.tsx
|
|
||||||
- /**/*.ts
|
|
||||||
- job: check-types
|
|
||||||
paths:
|
|
||||||
- /**/*.tsx
|
|
||||||
- /**/*.ts
|
|
||||||
- job: e2e
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/**
|
|
||||||
- job: e2e-embed
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/embeds/**
|
|
||||||
- job: test
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/**
|
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -1,33 +1,17 @@
|
|||||||
name: Unit tests
|
name: Unit tests
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/**
|
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: [Crowdin Action]
|
workflows: [Crowdin Action]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: TODO to Issue
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
MANUAL_COMMIT_REF:
|
||||||
|
description: "The SHA of the commit to get the diff for"
|
||||||
|
required: true
|
||||||
|
MANUAL_BASE_REF:
|
||||||
|
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
||||||
|
required: false
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
todos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: "Add TODOs to issues"
|
||||||
|
uses: "alstr/todo-to-issue-action@v4"
|
||||||
|
env:
|
||||||
|
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
||||||
|
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
||||||
|
with:
|
||||||
|
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: Yarn install
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
name: Yarn install & cache
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
@@ -77,6 +77,9 @@ lint-results
|
|||||||
#Storybook
|
#Storybook
|
||||||
apps/storybook/build-storybook.log
|
apps/storybook/build-storybook.log
|
||||||
|
|
||||||
|
# Infisical
|
||||||
|
.infisical.json
|
||||||
|
|
||||||
# Snaplet
|
# Snaplet
|
||||||
.snaplet/snapshots
|
.snaplet/snapshots
|
||||||
.snaplet/structure.d.ts
|
.snaplet/structure.d.ts
|
||||||
@@ -86,3 +89,4 @@ apps/storybook/build-storybook.log
|
|||||||
apps/api
|
apps/api
|
||||||
apps/website
|
apps/website
|
||||||
apps/console
|
apps/console
|
||||||
|
apps/auth
|
||||||
Vendored
+12
-30
@@ -2,38 +2,20 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Next.js: Server",
|
"type": "node",
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run dev",
|
"name": "Next.js Node Debug",
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"runtimeExecutable": "${workspaceFolder}/node_modules/next/dist/bin/next",
|
||||||
"outFiles": [
|
"env": {
|
||||||
"${workspaceFolder}/**/*.js",
|
"NODE_OPTIONS": "--inspect"
|
||||||
"!**/node_modules/**"
|
},
|
||||||
],
|
"cwd": "${workspaceFolder}/apps/web",
|
||||||
"sourceMaps": true,
|
|
||||||
"resolveSourceMapLocations": [
|
|
||||||
"${workspaceFolder}/**",
|
|
||||||
"!**/node_modules/**"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Next.js: Client",
|
|
||||||
"type": "pwa-chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:3000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Next.js: Full Stack",
|
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
|
||||||
"command": "npm run dev",
|
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"serverReadyAction": {
|
"sourceMapPathOverrides": {
|
||||||
"pattern": "started server on .+, url: (https?://.+)",
|
"meteor://💻app/*": "${workspaceFolder}/*",
|
||||||
"uriFormat": "%s",
|
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
|
||||||
"action": "debugWithChrome"
|
"webpack://?:*/*": "${workspaceFolder}/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+55
-1
@@ -4,6 +4,60 @@ Contributions are what make the open source community such an amazing place to l
|
|||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||||
|
|
||||||
|
## Priorities
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Type of Issue
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Priority
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Minor improvements, non-core feature requests
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Low+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Low%20Priority-green">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Confusing UX (... but working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Medium+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Medium%20Priority-yellow">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Features (Booking page, availabilty, timezone calculation)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-High%20Priority-orange">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Bugs (Login, Booking page, Emails are not working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:Urgent">
|
||||||
|
<img src="https://img.shields.io/badge/-Urgent-red">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
The development branch is `main`. This is the branch that all pull
|
The development branch is `main`. This is the branch that all pull
|
||||||
@@ -74,6 +128,6 @@ If you get errors, be sure to fix them before committing.
|
|||||||
## Making a Pull Request
|
## Making a Pull Request
|
||||||
|
|
||||||
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
|
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
|
||||||
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. Se more about [Linking a pull request to an issue
|
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
|
||||||
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||||
- Be sure to fill the PR Template accordingly.
|
- Be sure to fill the PR Template accordingly.
|
||||||
@@ -25,21 +25,21 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||||
<a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||||
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
||||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
|
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-Free-brightgreen" alt="Pricing"></a>
|
||||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||||
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
|
||||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
@@ -57,18 +57,37 @@ Calendly and other scheduling tools are awesome. It made our lives massively eas
|
|||||||
|
|
||||||
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
||||||
|
|
||||||
## Product of the Month: April 2021
|
## Recognition
|
||||||
|
|
||||||
#### Support us on [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
#### Hacker News
|
||||||
|
|
||||||
|
<a href="https://news.ycombinator.com/item?id=34507672">
|
||||||
|
<img
|
||||||
|
style="width: 250px; height: 54px;" width="250" height="54"
|
||||||
|
alt="Featured on Hacker News"
|
||||||
|
src="https://hackernews-badge.vercel.app/api?id=34507672"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://news.ycombinator.com/item?id=26817795">
|
||||||
|
<img
|
||||||
|
style="width: 250px; height: 54px;" width="250" height="54"
|
||||||
|
alt="Featured on Hacker News"
|
||||||
|
src="https://hackernews-badge.vercel.app/api?id=26817795"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/)
|
- [Next.js](https://nextjs.org/?ref=cal.com)
|
||||||
- [React](https://reactjs.org/)
|
- [tRPC](https://trpc.io/?ref=cal.com)
|
||||||
- [Tailwind](https://tailwindcss.com/)
|
- [React](https://reactjs.org/?ref=cal.com)
|
||||||
- [Prisma](https://prisma.io/)
|
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
||||||
|
- [Prisma](https://prisma.io/?ref=cal.com)
|
||||||
|
|
||||||
## Stay Up-to-Date
|
## Stay Up-to-Date
|
||||||
|
|
||||||
@@ -321,7 +340,27 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
|
<!-- BOUNTIES -->
|
||||||
|
|
||||||
|
### Bounties
|
||||||
|
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open">
|
||||||
|
<picture>
|
||||||
|
<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">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/calcom/cal.com/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- TRANSLATIONS -->
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
@@ -330,7 +369,9 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
|
|||||||
|
|
||||||
                                             except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"plan": "heroku-postgresql:hobby-dev"
|
"plan": "heroku-postgresql:mini"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
@@ -43,15 +43,18 @@
|
|||||||
},
|
},
|
||||||
"SENDGRID_API_KEY": {
|
"SENDGRID_API_KEY": {
|
||||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_SYNC_API_KEY": {
|
"SENDGRID_SYNC_API_KEY": {
|
||||||
"description": "Sendgrid internal sync service",
|
"description": "Sendgrid internal sync service",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_EMAIL": {
|
"SENDGRID_EMAIL": {
|
||||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"EMAIL_FROM": {
|
"EMAIL_FROM": {
|
||||||
"description": "Configures the global From: header whilst sending emails",
|
"description": "Configures the global From: header whilst sending emails",
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<!-- PROJECT LOGO -->
|
|
||||||
<div align="right">
|
|
||||||
<a href="https://github.com/calcom/cal.com">
|
|
||||||
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
|
|
||||||
</a>
|
|
||||||
<a href="https://cal.com">Website</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/calcom/cal.com/issues">Community Support</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Cal.com Documentation
|
|
||||||
|
|
||||||
The official product, support and developer documentation, containing information and guides about using the product as well as support for self-hosted installations. This documentation site runs on [Nextra](https://nextra.vercel.app), so you may refer to their documentation should you need information on anything that isn't covered here.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Git
|
|
||||||
- Node.js & npm
|
|
||||||
- Yarn
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Firstly, clone the repository using Git:
|
|
||||||
|
|
||||||
```console
|
|
||||||
git clone https://github.com/calcom/docs.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, you can install the dependencies with yarn:
|
|
||||||
|
|
||||||
```console
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Editing
|
|
||||||
|
|
||||||
To create, edit and delete documentation pages, you can simply create markdown (.mdx) files in the `pages/` folder. You can edit Markdown with any text editor, but VS Code and WebStorm have side-by-side previews so you can see your formatted content whilst writing markdown.
|
|
||||||
|
|
||||||
You will also need to add it as an entry to the `meta.json` file found in whichever directory that the .mdx file is in.
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
```console
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```console
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static content hosting service.
|
|
||||||
|
|
||||||
## How to easily contribute
|
|
||||||
|
|
||||||
## Existing Page
|
|
||||||
|
|
||||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
|
||||||
2. From here you can view all current pages on the documentation site. Select the page you would like to contribute to.
|
|
||||||
3. You should now be able to view the page you have selected. Located at the top right of the page will be a pencil icon. Pressing this will bring you up an editor to edit and make changes. You can add formatting using the buttons at the top, which will automatically insert the relevant markdown content needed to style the text.
|
|
||||||
4. From here make the changes you wish to make.
|
|
||||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
|
||||||
6. Your changes have been saved, to submit them for review, located on your screen, press the green 'Create Pull Request' button.
|
|
||||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
|
||||||
|
|
||||||
You have now successfully edited and submitted changes to our documentation site.
|
|
||||||
|
|
||||||
## Creating a New Page
|
|
||||||
|
|
||||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
|
||||||
2. From here you can view all current pages on the documentation site. At the top of your screen press the 'New file' button.
|
|
||||||
3. You should now be able to view the page you have created. Remember when renaming the document to put .mdx at the end of the file name.
|
|
||||||
4. From here make the changes you wish to make. Such as creating a title, sub-title and body text.
|
|
||||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
|
||||||
6. Your changes have been saved, to submit them for review, located on your screen, press the greem 'Create Pull Request' button.
|
|
||||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
|
||||||
|
|
||||||
You have now successfully created and submitted changes to our documentation site.
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iframe-resizer-react": "^1.1.0",
|
"iframe-resizer-react": "^1.1.0",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"nextra": "^1.1.0",
|
"nextra": "^1.1.0",
|
||||||
"nextra-theme-docs": "^1.2.2",
|
"nextra-theme-docs": "^1.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -25,6 +25,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
"eslint": "^8.22.0"
|
"eslint": "^8.34.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import "nextra-theme-docs/style.css";
|
import "nextra-theme-docs/style.css";
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { create } from "@storybook/theming";
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: "light",
|
||||||
|
colorPrimary: "#111827",
|
||||||
|
colorSecondary: "#111827",
|
||||||
|
appContentBg: "#F9FAFC",
|
||||||
|
brandTitle: "Cal.com UI",
|
||||||
|
brandUrl: "https://cal.com",
|
||||||
|
brandImage: "https://cal.com/logo.svg",
|
||||||
|
brandTarget: "_self",
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from "@storybook/addons";
|
||||||
|
|
||||||
|
import yourTheme from "./YourTheme";
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: yourTheme,
|
||||||
|
});
|
||||||
@@ -1,21 +1,7 @@
|
|||||||
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
||||||
|
|
||||||
const withTM = require("next-transpile-modules")([
|
|
||||||
"@calcom/app-store",
|
|
||||||
"@calcom/dayjs",
|
|
||||||
"@calcom/emails",
|
|
||||||
"@calcom/trpc",
|
|
||||||
"@calcom/embed-core",
|
|
||||||
"@calcom/embed-react",
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/lib",
|
|
||||||
"@calcom/prisma",
|
|
||||||
"@calcom/ui",
|
|
||||||
]);
|
|
||||||
const glob = require("glob");
|
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
@@ -27,7 +13,18 @@ const nextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
experimental: { images: { allowFutureImage: true } },
|
transpilePackages: [
|
||||||
|
"@calcom/app-store",
|
||||||
|
"@calcom/dayjs",
|
||||||
|
"@calcom/emails",
|
||||||
|
"@calcom/trpc",
|
||||||
|
"@calcom/embed-core",
|
||||||
|
"@calcom/embed-react",
|
||||||
|
"@calcom/features",
|
||||||
|
"@calcom/lib",
|
||||||
|
"@calcom/prisma",
|
||||||
|
"@calcom/ui",
|
||||||
|
],
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.0",
|
"@radix-ui/react-popover": "^1.0.2",
|
||||||
"@radix-ui/react-radio-group": "^1.0.0",
|
"@radix-ui/react-radio-group": "^1.0.0",
|
||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"storybook-addon-rtl-direction": "^0.0.19"
|
"storybook-addon-rtl-direction": "^0.0.19"
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"storybook-addon-next": "^1.6.9",
|
"storybook-addon-next": "^1.6.9",
|
||||||
"storybook-react-i18next": "^1.1.2",
|
"storybook-react-i18next": "^1.1.2",
|
||||||
"tailwindcss": "^3.2.1",
|
"tailwindcss": "^3.2.1",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^2.9.15"
|
"vite": "^2.9.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,4 +2,9 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import "../../../packages/ui/styles/shared-globals.css"
|
@import "../../../packages/ui/styles/shared-globals.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-inter: "Inter var";
|
||||||
|
--font-cal: "Cal Sans";
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"isarray": "2.0.5",
|
"isarray": "2.0.5",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"openapi-snippet": "^0.13.0",
|
"openapi-snippet": "^0.13.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { SwaggerUI } from "swagger-ui-react";
|
import type { SwaggerUI } from "swagger-ui-react";
|
||||||
|
|
||||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui";
|
import { FiX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -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>
|
||||||
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { OptionProps } from "react-select";
|
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";
|
||||||
|
|||||||
@@ -1,35 +1,93 @@
|
|||||||
import { ReactNode } from "react";
|
import { useRouter } from "next/router";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
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 { Badge, ListItemText } from "@calcom/ui";
|
import { Badge, ListItemText } from "@calcom/ui";
|
||||||
|
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface AppListCardProps {
|
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||||
|
|
||||||
|
type AppListCardProps = {
|
||||||
logo?: string;
|
logo?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
isTemplate?: boolean;
|
||||||
|
invalidCredential?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
} & ShouldHighlight;
|
||||||
|
|
||||||
|
const schema = z.object({ hl: z.string().optional() });
|
||||||
|
|
||||||
export default function AppListCard(props: AppListCardProps) {
|
export default function AppListCard(props: AppListCardProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { logo, title, description, actions, isDefault } = props;
|
const {
|
||||||
|
logo,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
isDefault,
|
||||||
|
slug,
|
||||||
|
shouldHighlight,
|
||||||
|
isTemplate,
|
||||||
|
invalidCredential,
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
data: { hl },
|
||||||
|
} = useTypedQuery(schema);
|
||||||
|
const router = useRouter();
|
||||||
|
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldHighlight && highlight) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setHighlight(false);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete("hl");
|
||||||
|
router.replace(url.pathname, undefined, { shallow: true });
|
||||||
|
}, 3000);
|
||||||
|
timeoutRef.current = timer;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex gap-x-3 p-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="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||||
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
|
<div className="flex items-center gap-x-2">
|
||||||
|
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||||
|
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListItemText component="p">{description}</ListItemText>
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
|
{invalidCredential && (
|
||||||
|
<div className="flex gap-x-2 pt-2">
|
||||||
|
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||||
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
|
{t("invalid_credential")}
|
||||||
|
</ListItemText>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
|
{children && <div className="w-full">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NextRouter, useRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { useRouter } from "next/router";
|
||||||
import { components, ControlProps } from "react-select";
|
import type { MutableRefObject, RefObject } from "react";
|
||||||
|
import { createRef, forwardRef, useRef, useState } from "react";
|
||||||
|
import type { ControlProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -12,16 +15,15 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
Icon,
|
|
||||||
InputLeading,
|
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextField,
|
TextField,
|
||||||
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
@@ -488,7 +490,7 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
href: "embedTabName=embed-code",
|
href: "embedTabName=embed-code",
|
||||||
icon: Icon.FiCode,
|
icon: FiCode,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -541,7 +543,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
{
|
{
|
||||||
name: "React",
|
name: "React",
|
||||||
href: "embedTabName=embed-react",
|
href: "embedTabName=embed-react",
|
||||||
icon: Icon.FiCode,
|
icon: FiCode,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -581,7 +583,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
{
|
{
|
||||||
name: "Preview",
|
name: "Preview",
|
||||||
href: "embedTabName=embed-preview",
|
href: "embedTabName=embed-preview",
|
||||||
icon: Icon.FiTrello,
|
icon: FiTrello,
|
||||||
type: "iframe",
|
type: "iframe",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||||
@@ -617,7 +619,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}>
|
||||||
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -676,7 +678,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
||||||
const embedCodeRefs: Record<typeof tabs[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
||||||
tabs
|
tabs
|
||||||
.filter((tab) => tab.type === "code")
|
.filter((tab) => tab.type === "code")
|
||||||
.forEach((codeTab) => {
|
.forEach((codeTab) => {
|
||||||
@@ -720,7 +722,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
const calLink = decodeURIComponent(embedUrl);
|
const calLink = decodeURIComponent(embedUrl);
|
||||||
|
|
||||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
const addToPalette = (update: (typeof previewState)["palette"]) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
return {
|
return {
|
||||||
...previewState,
|
...previewState,
|
||||||
@@ -815,7 +817,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
<Icon.FiArrowLeft className="mr-4 w-4" />
|
<FiArrowLeft className="mr-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -835,7 +837,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
? "Floating Popup Customization"
|
? "Floating Popup Customization"
|
||||||
: "Element Click Customization"}
|
: "Element Click Customization"}
|
||||||
</div>
|
</div>
|
||||||
<Icon.FiChevronRight
|
<FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
} ml-auto h-5 w-5 text-gray-500`}
|
||||||
@@ -863,7 +865,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
addOnLeading={<InputLeading>W</InputLeading>}
|
addOnLeading={<>W</>}
|
||||||
/>
|
/>
|
||||||
<span className="p-2">×</span>
|
<span className="p-2">×</span>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -883,7 +885,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
addOnLeading={<InputLeading>H</InputLeading>}
|
addOnLeading={<>H</>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1002,7 +1004,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
<CollapsibleTrigger className="flex w-full" type="button">
|
||||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
||||||
<Icon.FiChevronRight
|
<FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
} ml-auto h-5 w-5 text-gray-500`}
|
||||||
@@ -1061,7 +1063,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
addToPalette({
|
addToPalette({
|
||||||
[palette.name as keyof typeof previewState["palette"]]: color,
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Dispatch, useState, useEffect } from "react";
|
import type { Dispatch } from "react";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { useState, useEffect } from "react";
|
||||||
|
import type { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
||||||
|
|
||||||
import { Area, getCroppedImg } from "@lib/cropImage";
|
import type { Area } from "@lib/cropImage";
|
||||||
|
import { getCroppedImg } from "@lib/cropImage";
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
import { useFileReader } from "@lib/hooks/useFileReader";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Slider from "@components/Slider";
|
import Slider from "@components/Slider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import noop from "lodash/noop";
|
||||||
import Link, { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, Fragment, MouseEventHandler } from "react";
|
import type { FC, MouseEventHandler } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
export interface NavTabProps {
|
export interface NavTabProps {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|||||||
@@ -1,40 +1,42 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary, Icon } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/profile",
|
href: "/settings/my-account/profile",
|
||||||
icon: Icon.FiUser,
|
icon: FiUser,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teams",
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: Icon.FiUsers,
|
icon: FiUsers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "security",
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: Icon.FiKey,
|
icon: FiKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "developer",
|
name: "developer",
|
||||||
href: "/settings/developer",
|
href: "/settings/developer",
|
||||||
icon: Icon.FiTerminal,
|
icon: FiTerminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "billing",
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: Icon.FiCreditCard,
|
icon: FiCreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: Icon.FiLock,
|
icon: FiLock,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { OptionProps } from "react-select";
|
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 type { App } from "@calcom/types/App";
|
||||||
import { Button, Icon, Select } from "@calcom/ui";
|
import { Button, Select } from "@calcom/ui";
|
||||||
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
||||||
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||||
<p>{t("install_new_calendar_app")}</p>
|
<p>{t("install_new_calendar_app")}</p>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -61,7 +62,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<Select
|
<Select
|
||||||
name="additionalCalendar"
|
name="additionalCalendar"
|
||||||
placeholder={
|
placeholder={
|
||||||
<Button StartIcon={Icon.FiPlus} color="secondary">
|
<Button StartIcon={FiPlus} color="secondary">
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,18 @@ import classNames from "@calcom/lib/classNames";
|
|||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
|
import {
|
||||||
|
FiBookOpen,
|
||||||
|
FiCheck,
|
||||||
|
FiExternalLink,
|
||||||
|
FiFile,
|
||||||
|
FiFlag,
|
||||||
|
FiMail,
|
||||||
|
FiPlus,
|
||||||
|
FiShield,
|
||||||
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const Component = ({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
@@ -118,7 +128,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={Icon.FiCheck} color="secondary" disabled>
|
<Button StartIcon={FiCheck} 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")}
|
||||||
@@ -139,7 +149,7 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
StartIcon={Icon.FiPlus}
|
StartIcon={FiPlus}
|
||||||
{...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.
|
||||||
@@ -200,7 +210,7 @@ const Component = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="prose prose-sm mt-8">{body}</div>
|
<div className="prose-sm prose mt-8">{body}</div>
|
||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||||
<span>
|
<span>
|
||||||
{price === 0 ? (
|
{price === 0 ? (
|
||||||
@@ -226,7 +236,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-sm font-normal text-black no-underline hover:underline"
|
className="text-sm font-normal text-black no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -238,7 +248,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="font-normal text-black no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -250,7 +260,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="font-normal text-black no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -263,7 +273,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="font-normal text-black no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -275,7 +285,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="font-normal text-black no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -286,13 +296,18 @@ const Component = ({
|
|||||||
{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}`}>
|
||||||
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ShellHeading = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return <span className="block py-2">{t("app_store")}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function App(props: {
|
export default function App(props: {
|
||||||
name: string;
|
name: string;
|
||||||
description: AppType["description"];
|
description: AppType["description"];
|
||||||
@@ -318,18 +333,12 @@ export default function App(props: {
|
|||||||
images?: string[];
|
images?: string[];
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={props.name}
|
title={props.name}
|
||||||
description={props.description}
|
description={props.description}
|
||||||
app={{ slug: props.logo, name: props.name, description: props.description }}
|
app={{ slug: props.logo, name: props.name, description: props.description }}
|
||||||
nextSeoProps={{
|
|
||||||
nofollow: true,
|
|
||||||
noindex: true,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{props.licenseRequired ? (
|
{props.licenseRequired ? (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
|
||||||
List,
|
List,
|
||||||
showToast,
|
showToast,
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
Switch,
|
Switch,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
|
import AppListCard from "@components/AppListCard";
|
||||||
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
|
||||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{!!props.destination && (
|
{!!props.destination && (
|
||||||
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
<FiArrowLeft className="h-4 w-4" />
|
||||||
{t("adding_events_to")}
|
{t("adding_events_to")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -118,13 +118,13 @@ function CalendarList(props: Props) {
|
|||||||
success={({ data }) => (
|
success={({ data }) => (
|
||||||
<List>
|
<List>
|
||||||
{data.items.map((item) => (
|
{data.items.map((item) => (
|
||||||
<IntegrationListItem
|
<AppListCard
|
||||||
name={item.name}
|
title={item.name}
|
||||||
slug={item.slug}
|
key={item.name}
|
||||||
key={item.title}
|
|
||||||
title={item.title}
|
|
||||||
logo={item.logo}
|
logo={item.logo}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
|
shouldHighlight
|
||||||
|
slug={item.slug}
|
||||||
actions={
|
actions={
|
||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={item.type}
|
type={item.type}
|
||||||
@@ -161,16 +161,16 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
<List>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
<Fragment key={item.credentialId}>
|
<Fragment key={item.credentialId}>
|
||||||
{item.calendars ? (
|
{item.calendars ? (
|
||||||
<IntegrationListItem
|
<AppListCard
|
||||||
|
shouldHighlight
|
||||||
slug={item.integration.slug}
|
slug={item.integration.slug}
|
||||||
title={item.integration.title}
|
title={item.integration.name}
|
||||||
logo={item.integration.logo}
|
logo={item.integration.logo}
|
||||||
description={item.primary?.email ?? item.integration.description}
|
description={item.primary?.email ?? item.integration.description}
|
||||||
separate={true}
|
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-32 justify-end">
|
<div className="flex w-32 justify-end">
|
||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
@@ -200,7 +200,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</IntegrationListItem>
|
</AppListCard>
|
||||||
) : (
|
) : (
|
||||||
<Alert
|
<Alert
|
||||||
severity="warning"
|
severity="warning"
|
||||||
@@ -282,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
<div className="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="relative rounded-md border border-gray-200 bg-white p-1.5">
|
||||||
<Icon.FiCalendar className="h-8 w-8" strokeWidth="1" />
|
<FiCalendar className="h-8 w-8" strokeWidth="1" />
|
||||||
<Icon.FiPlus
|
<FiPlus
|
||||||
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
/>
|
/>
|
||||||
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Icon.FiCalendar}
|
Icon={FiCalendar}
|
||||||
headline={t("no_category_apps", {
|
headline={t("no_category_apps", {
|
||||||
category: t("calendar").toLowerCase(),
|
category: t("calendar").toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
|
|
||||||
function IntegrationListItem(props: {
|
|
||||||
imageSrc?: string;
|
|
||||||
slug: string;
|
|
||||||
name?: string;
|
|
||||||
title?: string;
|
|
||||||
description: string;
|
|
||||||
actions?: ReactNode;
|
|
||||||
children?: ReactNode;
|
|
||||||
logo: string;
|
|
||||||
destination?: boolean;
|
|
||||||
separate?: boolean;
|
|
||||||
invalidCredential?: boolean;
|
|
||||||
isTemplate?: boolean;
|
|
||||||
}): JSX.Element {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const router = useRouter();
|
|
||||||
const { hl } = router.query;
|
|
||||||
const [highlight, setHighlight] = useState(hl === props.slug);
|
|
||||||
const title = props.name || props.title;
|
|
||||||
|
|
||||||
// The highlight is to show a newly installed app, coming from the app's
|
|
||||||
// redirection after installation, so we proceed to show the corresponding
|
|
||||||
// message
|
|
||||||
if (highlight) {
|
|
||||||
showToast(t("app_successfully_installed"), "success");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => setHighlight(false), 3000);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
expanded={!!props.children}
|
|
||||||
className={classNames(
|
|
||||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
|
||||||
"my-0 flex-col border transition-colors duration-500",
|
|
||||||
highlight ? "bg-yellow-100" : ""
|
|
||||||
)}>
|
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
|
||||||
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
|
|
||||||
<div className="flex-grow truncate pl-2">
|
|
||||||
<ListItemTitle component="h3" className="flex ">
|
|
||||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
|
||||||
{props.isTemplate && (
|
|
||||||
<Badge variant="red" className="ml-4">
|
|
||||||
Template
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</ListItemTitle>
|
|
||||||
<ListItemText component="p">{props.description}</ListItemText>
|
|
||||||
{/* Alert error that key stopped working. */}
|
|
||||||
{props.invalidCredential && (
|
|
||||||
<div className="flex items-center space-x-2 rtl:space-x-reverse">
|
|
||||||
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
|
||||||
{t("invalid_credential")}
|
|
||||||
</ListItemText>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>{props.actions}</div>
|
|
||||||
</div>
|
|
||||||
{props.children && <div className="w-full">{props.children}</div>}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntegrationListItem;
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
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, Icon } from "@calcom/ui";
|
import { EmptyScreen } from "@calcom/ui";
|
||||||
|
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -25,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={Icon.FiAlertCircle}
|
Icon={FiAlertCircle}
|
||||||
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") : ""}
|
||||||
|
|||||||
@@ -1,42 +1,43 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
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 { Icon } from "@calcom/ui";
|
|
||||||
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";
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
name: "calendar",
|
name: "calendar",
|
||||||
href: "/apps/installed/calendar",
|
href: "/apps/installed/calendar",
|
||||||
icon: Icon.FiCalendar,
|
icon: FiCalendar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conferencing",
|
name: "conferencing",
|
||||||
href: "/apps/installed/conferencing",
|
href: "/apps/installed/conferencing",
|
||||||
icon: Icon.FiVideo,
|
icon: FiVideo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payment",
|
name: "payment",
|
||||||
href: "/apps/installed/payment",
|
href: "/apps/installed/payment",
|
||||||
icon: Icon.FiCreditCard,
|
icon: FiCreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "automation",
|
name: "automation",
|
||||||
href: "/apps/installed/automation",
|
href: "/apps/installed/automation",
|
||||||
icon: Icon.FiShare2,
|
icon: FiShare2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "analytics",
|
name: "analytics",
|
||||||
href: "/apps/installed/analytics",
|
href: "/apps/installed/analytics",
|
||||||
icon: Icon.FiBarChart,
|
icon: FiBarChart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other",
|
name: "other",
|
||||||
href: "/apps/installed/other",
|
href: "/apps/installed/other",
|
||||||
icon: Icon.FiGrid,
|
icon: FiGrid,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export default function InstalledAppsLayout({
|
|||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
const variant: typeof InstalledAppVariants[number] = "payment";
|
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
||||||
|
|
||||||
const query = trpc.viewer.integrations.useQuery({
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
variant,
|
variant,
|
||||||
@@ -57,9 +58,7 @@ export default function InstalledAppsLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest}>
|
||||||
<AppCategoryNavigation
|
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||||
baseURL="/apps/installed"
|
|
||||||
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
|
|
||||||
{children}
|
{children}
|
||||||
</AppCategoryNavigation>
|
</AppCategoryNavigation>
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps, useEffect } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
import { UserPermissionRole } from ".prisma/client";
|
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
|
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
@@ -22,10 +23,11 @@ export default function AdminLayout({
|
|||||||
}
|
}
|
||||||
}, [session, router]);
|
}, [session, router]);
|
||||||
|
|
||||||
|
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="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
|
||||||
<div className="flex flex-1 [&>*]:flex-1">
|
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
|
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -18,17 +19,21 @@ export default SkeletonLoader;
|
|||||||
|
|
||||||
function SkeletonItem() {
|
function SkeletonItem() {
|
||||||
return (
|
return (
|
||||||
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
|
<li>
|
||||||
<div className="flex-grow truncate text-sm">
|
<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 flex-col space-y-2">
|
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
|
||||||
<SkeletonText className="h-4 w-32" />
|
<SkeletonText className="my-1 h-4 w-32" />
|
||||||
<SkeletonText className="h-2 w-32" />
|
<SkeletonText className="my-1 h-2 w-24" />
|
||||||
</div>
|
<SkeletonText className="my-1 h-2 w-40" />
|
||||||
</div>
|
|
||||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
|
||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
|
||||||
<SkeletonText className="h-6 w-12" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mx-5"
|
||||||
|
type="button"
|
||||||
|
variant="icon"
|
||||||
|
color="secondary"
|
||||||
|
StartIcon={FiMoreHorizontal}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
|
import { FiLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
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 ? (
|
return locations.length ? (
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||||
{locations.map((location) => {
|
{locations.map((location, index) => {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
// It's possible that the location app got uninstalled
|
// It's possible that the location app got uninstalled
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<Icon.FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
||||||
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
|
<p className="line-clamp-1">{t(locationKeyToString(location) ?? "")}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
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 type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
@@ -37,6 +41,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
@@ -46,123 +51,134 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
||||||
}, []);
|
}, []);
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
if (!date) return null;
|
const ref = useCallback(
|
||||||
|
(node: HTMLDivElement) => {
|
||||||
|
if (isMobile) {
|
||||||
|
node?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
<div ref={slotPickerRef}>
|
||||||
<div className="mb-6 flex items-center text-left text-base">
|
{!!date ? (
|
||||||
<div className="mr-4">
|
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
<div className="mb-6 flex items-center text-left text-base">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
<div className="mr-4">
|
||||||
</span>
|
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||||
<span className="text-bookinglight font-medium">
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
</span>
|
||||||
</span>
|
<span className="text-bookinglight font-medium">
|
||||||
</div>
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
<div className="ml-auto">
|
</span>
|
||||||
<ToggleGroup
|
</div>
|
||||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
<div className="ml-auto">
|
||||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
<ToggleGroup
|
||||||
options={[
|
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||||
{ value: "12", label: t("12_hour_short") },
|
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||||
{ value: "24", label: t("24_hour_short") },
|
options={[
|
||||||
]}
|
{ value: "12", label: t("12_hour_short") },
|
||||||
/>
|
{ value: "24", label: t("24_hour_short") },
|
||||||
</div>
|
]}
|
||||||
</div>
|
/>
|
||||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
</div>
|
||||||
{slots.length > 0 &&
|
|
||||||
slots.map((slot) => {
|
|
||||||
type BookingURL = {
|
|
||||||
pathname: string;
|
|
||||||
query: Record<string, string | number | string[] | undefined>;
|
|
||||||
};
|
|
||||||
const bookingUrl: BookingURL = {
|
|
||||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
|
||||||
type: eventTypeId,
|
|
||||||
slug: eventTypeSlug,
|
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (rescheduleUid) {
|
|
||||||
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If event already has an attendee add booking id
|
|
||||||
if (slot.bookingUid) {
|
|
||||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
|
||||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
|
||||||
{/* Current there is no way to disable Next.js Links */}
|
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
|
||||||
<div
|
|
||||||
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 ",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}>
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href={bookingUrl}
|
|
||||||
prefetch={false}
|
|
||||||
className={classNames(
|
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}
|
|
||||||
data-testid="time">
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && (
|
|
||||||
<p
|
|
||||||
className={`${
|
|
||||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
|
||||||
? "text-rose-600"
|
|
||||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
|
||||||
? "text-yellow-500"
|
|
||||||
: "text-emerald-400"
|
|
||||||
} text-sm`}>
|
|
||||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
|
||||||
{seatsPerTimeSlot} {t("seats_available")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{!isLoading && !slots.length && (
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||||
|
{slots.length > 0 &&
|
||||||
|
slots.map((slot) => {
|
||||||
|
type BookingURL = {
|
||||||
|
pathname: string;
|
||||||
|
query: Record<string, string | number | string[] | undefined>;
|
||||||
|
};
|
||||||
|
const bookingUrl: BookingURL = {
|
||||||
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||||
|
type: eventTypeId,
|
||||||
|
slug: eventTypeSlug,
|
||||||
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
{isLoading && !slots.length && (
|
if (rescheduleUid) {
|
||||||
<>
|
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
||||||
<SkeletonContainer className="mb-2">
|
}
|
||||||
<SkeletonText className="h-5 w-full" />
|
|
||||||
</SkeletonContainer>
|
// If event already has an attendee add booking id
|
||||||
<SkeletonContainer className="mb-2">
|
if (slot.bookingUid) {
|
||||||
<SkeletonText className="h-5 w-full" />
|
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||||
</SkeletonContainer>
|
}
|
||||||
<SkeletonContainer className="mb-2">
|
|
||||||
<SkeletonText className="h-5 w-full" />
|
return (
|
||||||
</SkeletonContainer>
|
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||||
</>
|
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||||
)}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
</div>
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
|
<div
|
||||||
|
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 ",
|
||||||
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
|
)}>
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={bookingUrl}
|
||||||
|
prefetch={false}
|
||||||
|
className={classNames(
|
||||||
|
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
||||||
|
"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" ? "" : ""
|
||||||
|
)}
|
||||||
|
data-testid="time">
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && (
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||||
|
? "text-rose-600"
|
||||||
|
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-emerald-400"
|
||||||
|
} text-sm`}>
|
||||||
|
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||||
|
{seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!isLoading && !slots.length && (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !slots.length && (
|
||||||
|
<>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode, useEffect } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
|
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -92,15 +94,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
"flex",
|
"flex",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||||
<Icon.FiInfo
|
|
||||||
className={classNames(
|
|
||||||
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
|
||||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
|
|
||||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +106,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
</div>
|
</div>
|
||||||
{requiresConfirmationText}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
@@ -123,9 +117,10 @@ 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 && "dark:text-darkgray-600 text-gray-600",
|
||||||
|
!eventType.metadata?.multipleDuration && "items-center"
|
||||||
)}>
|
)}>
|
||||||
<Icon.FiClock
|
<FiClock
|
||||||
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]",
|
||||||
isBookingPage && "mt-[2px]"
|
isBookingPage && "mt-[2px]"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
import { FiCreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
||||||
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookingDescriptionPayment;
|
||||||
@@ -2,14 +2,17 @@ import { BookingStatus } from "@prisma/client";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
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";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { ActionType } from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,15 +20,13 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
|
||||||
Icon,
|
|
||||||
MeetingTimeInTimezones,
|
MeetingTimeInTimezones,
|
||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionType,
|
|
||||||
TableActions,
|
TableActions,
|
||||||
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
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) {
|
||||||
setRejectionDialogIsOpen(false);
|
setRejectionDialogIsOpen(false);
|
||||||
showToast(t("booking_rejection_success"), "success");
|
showToast(t("booking_rejection_success"), "success");
|
||||||
} else {
|
} else {
|
||||||
@@ -100,7 +101,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: Icon.FiSlash,
|
icon: FiSlash,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,7 +110,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
bookingConfirm(true);
|
bookingConfirm(true);
|
||||||
},
|
},
|
||||||
icon: Icon.FiCheck,
|
icon: FiCheck,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
color: "primary",
|
color: "primary",
|
||||||
},
|
},
|
||||||
@@ -135,7 +136,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
href: `/booking/${booking.uid}?cancel=true${
|
href: `/booking/${booking.uid}?cancel=true${
|
||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}`,
|
}`,
|
||||||
icon: Icon.FiX,
|
icon: FiX,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "edit_booking",
|
id: "edit_booking",
|
||||||
@@ -143,13 +144,13 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "reschedule",
|
||||||
icon: Icon.FiClock,
|
icon: FiClock,
|
||||||
label: t("reschedule_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}`,
|
href: `/reschedule/${booking.uid}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: Icon.FiSend,
|
icon: FiSend,
|
||||||
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: () => {
|
||||||
@@ -162,7 +163,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenLocationDialog(true);
|
setIsOpenLocationDialog(true);
|
||||||
},
|
},
|
||||||
icon: Icon.FiMapPin,
|
icon: FiMapPin,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -179,7 +180,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||||
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
<FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -222,6 +223,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
};
|
};
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons =
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
|
const title = decodeURIComponent(booking.title);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -245,24 +248,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||||
<DialogHeader title={t("rejection_reason_title")} />
|
<div>
|
||||||
|
<TextAreaField
|
||||||
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
|
name="rejectionReason"
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
label={
|
||||||
{t("rejection_reason")}
|
<>
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
{t("rejection_reason")}
|
||||||
</p>
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||||
<TextArea
|
</>
|
||||||
name={t("rejection_reason")}
|
}
|
||||||
value={rejectionReason}
|
value={rejectionReason}
|
||||||
onChange={(e) => setRejectionReason(e.target.value)}
|
onChange={(e) => setRejectionReason(e.target.value)}
|
||||||
className="mb-5 sm:mb-6"
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -355,12 +357,12 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div
|
<div
|
||||||
title={booking.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-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{booking.title}
|
{title}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
@@ -455,7 +457,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
);
|
);
|
||||||
})}>
|
})}>
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="text-gray-600 dark:text-white">
|
||||||
<Icon.FiRefreshCcw
|
<FiRefreshCcw
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
import { Button, TextArea } from "@calcom/ui";
|
||||||
|
import { FiX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -28,7 +29,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { booking, profile, team, allRemainingBookings } = props;
|
const { booking, allRemainingBookings } = 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"));
|
||||||
@@ -39,7 +40,7 @@ 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="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
<FiX className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<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-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import type { EventType } from "@prisma/client";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
|
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
|
||||||
|
|
||||||
|
const getRefetchInterval = (refetchCount: number): number => {
|
||||||
|
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
|
||||||
|
return intervals[refetchCount] || intervals[intervals.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSlots = ({
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
usernameList,
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
enabled = true,
|
||||||
|
}: {
|
||||||
|
eventTypeId: number;
|
||||||
|
eventTypeSlug: string;
|
||||||
|
startTime?: Dayjs;
|
||||||
|
endTime?: Dayjs;
|
||||||
|
usernameList: string[];
|
||||||
|
timeZone?: string;
|
||||||
|
duration?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [refetchCount, setRefetchCount] = useState(0);
|
||||||
|
const refetchInterval = getRefetchInterval(refetchCount);
|
||||||
|
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
|
||||||
|
{
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
usernameList,
|
||||||
|
startTime: startTime?.toISOString() || "",
|
||||||
|
endTime: endTime?.toISOString() || "",
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!startTime && !!endTime && enabled,
|
||||||
|
refetchInterval,
|
||||||
|
trpc: { context: { skipBatch: true } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!data && fetchStatus === "idle") {
|
||||||
|
setRefetchCount(refetchCount + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fetchStatus, data]);
|
||||||
|
|
||||||
|
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
||||||
|
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlotPicker = ({
|
||||||
|
eventType,
|
||||||
|
timeFormat,
|
||||||
|
onTimeFormatChange,
|
||||||
|
timeZone,
|
||||||
|
recurringEventCount,
|
||||||
|
users,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
weekStart = 0,
|
||||||
|
ethSignature,
|
||||||
|
}: {
|
||||||
|
eventType: Pick<
|
||||||
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||||
|
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
||||||
|
>;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
seatsPerTimeSlot?: number;
|
||||||
|
recurringEventCount?: number;
|
||||||
|
users: string[];
|
||||||
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
ethSignature?: string;
|
||||||
|
}) => {
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
|
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
||||||
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!eventType.metadata?.multipleDuration) {
|
||||||
|
duration = eventType.length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
|
||||||
|
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
||||||
|
if (timeZone === "Etc/GMT") {
|
||||||
|
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
||||||
|
if (date) {
|
||||||
|
setSelectedDate(dayjs.utc(date));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set the start of the month without shifting time like startOf() may do.
|
||||||
|
setBrowsingDate(
|
||||||
|
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
||||||
|
);
|
||||||
|
if (date) {
|
||||||
|
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
||||||
|
setSelectedDate(dayjs.tz(date, timeZone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [router.isReady, month, date, duration, timeZone]);
|
||||||
|
|
||||||
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
|
const { slots: monthSlots, isLoading } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime:
|
||||||
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||||
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||||
|
: browsingDate?.startOf("month"),
|
||||||
|
endTime: browsingDate?.endOf("month"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime: selectedDate?.startOf("day"),
|
||||||
|
endTime: selectedDate?.endOf("day"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
/** Prevent refetching is we already have this data from month slots */
|
||||||
|
enabled: !!selectedDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Hide skeleton if we have the slot loaded in the month query */
|
||||||
|
const isLoadingSelectedDateSlots = (() => {
|
||||||
|
if (!selectedDate) return _isLoadingSelectedDateSlots;
|
||||||
|
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DatePicker
|
||||||
|
isLoading={isLoading}
|
||||||
|
className={classNames(
|
||||||
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 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"
|
||||||
|
)}
|
||||||
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||||
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
|
selected={selectedDate}
|
||||||
|
onChange={(newDate) => {
|
||||||
|
setDate(newDate.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
|
onMonthChange={(newMonth) => {
|
||||||
|
setMonth(newMonth.format("YYYY-MM"));
|
||||||
|
}}
|
||||||
|
browsingDate={browsingDate}
|
||||||
|
weekStart={weekStart}
|
||||||
|
/>
|
||||||
|
<AvailableTimes
|
||||||
|
isLoading={isLoadingSelectedDateSlots}
|
||||||
|
slots={
|
||||||
|
selectedDate &&
|
||||||
|
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
|
||||||
|
monthSlots[selectedDate.format("YYYY-MM-DD")])
|
||||||
|
}
|
||||||
|
date={selectedDate}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onTimeFormatChange={onTimeFormatChange}
|
||||||
|
eventTypeId={eventType.id}
|
||||||
|
eventTypeSlug={eventType.slug}
|
||||||
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
|
recurringCount={recurringEventCount}
|
||||||
|
ethSignature={ethSignature}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
@@ -11,7 +15,8 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const { t } = useLocale();
|
const query = useMeQuery();
|
||||||
|
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -23,18 +28,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
const customStyles = {
|
||||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||||
<div className="mb-4 flex">
|
padding: "10px 12px !important",
|
||||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||||
</div>
|
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||||
<TimezoneSelect
|
...(userTheme === "dark" && {
|
||||||
id="timeZone"
|
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||||
value={selectedTimeZone}
|
}),
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
}),
|
||||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
control: () => ({
|
||||||
/>
|
display: "flex",
|
||||||
</div>
|
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 ? (
|
||||||
|
<TimezoneSelect
|
||||||
|
id="timeZone"
|
||||||
|
value={selectedTimeZone}
|
||||||
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
|
className="flex h-6 text-sm font-medium"
|
||||||
|
styles={customStyles}
|
||||||
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FiGlobe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
|
import TimeOptions from "@components/booking/TimeOptions";
|
||||||
|
|
||||||
|
export function TimezoneDropdown({
|
||||||
|
onChangeTimeZone,
|
||||||
|
}: {
|
||||||
|
onChangeTimeZone: (newTimeZone: string) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
}) {
|
||||||
|
const handleSelectTimeZone = (newTimeZone: string) => {
|
||||||
|
onChangeTimeZone(newTimeZone);
|
||||||
|
localStorageTimeZone(newTimeZone);
|
||||||
|
};
|
||||||
|
|
||||||
|
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] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
||||||
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||||
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
import type { AvatarGroupProps } from "@calcom/ui";
|
||||||
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
|
|
||||||
export const UserAvatars = ({
|
export const UserAvatars = ({
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import dynamic from "next/dynamic";
|
||||||
import { EventType } from "@prisma/client";
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
@@ -17,229 +14,39 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
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 { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { Icon, HeadSeo } from "@calcom/ui";
|
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import Gates from "@components/Gates";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import TimeOptions from "@components/booking/TimeOptions";
|
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
const useSlots = ({
|
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
usernameList,
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
}: {
|
|
||||||
eventTypeId: number;
|
|
||||||
eventTypeSlug: string;
|
|
||||||
startTime?: Dayjs;
|
|
||||||
endTime?: Dayjs;
|
|
||||||
usernameList: string[];
|
|
||||||
timeZone?: string;
|
|
||||||
duration?: string;
|
|
||||||
}) => {
|
|
||||||
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
|
||||||
{
|
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
usernameList,
|
|
||||||
startTime: startTime?.toISOString() || "",
|
|
||||||
endTime: endTime?.toISOString() || "",
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!startTime && !!endTime,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
if (data?.slots) {
|
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||||
setCachedSlots((c) => ({ ...c, ...data?.slots }));
|
ssr: false,
|
||||||
}
|
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
|
||||||
}, [data]);
|
});*/
|
||||||
|
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
|
||||||
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
ssr: false,
|
||||||
return { slots: cachedSlots, isLoading: isLoading || isPaused };
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const SlotPicker = ({
|
|
||||||
eventType,
|
|
||||||
timeFormat,
|
|
||||||
onTimeFormatChange,
|
|
||||||
timeZone,
|
|
||||||
recurringEventCount,
|
|
||||||
users,
|
|
||||||
seatsPerTimeSlot,
|
|
||||||
weekStart = 0,
|
|
||||||
ethSignature,
|
|
||||||
}: {
|
|
||||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
|
||||||
timeFormat: TimeFormat;
|
|
||||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
seatsPerTimeSlot?: number;
|
|
||||||
recurringEventCount?: number;
|
|
||||||
users: string[];
|
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
ethSignature?: string;
|
|
||||||
}) => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
|
||||||
const { duration } = useRouterQuery("duration");
|
|
||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) return;
|
|
||||||
|
|
||||||
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
|
||||||
if (timeZone === "Etc/GMT") {
|
|
||||||
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
|
||||||
if (date) {
|
|
||||||
setSelectedDate(dayjs.utc(date));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set the start of the month without shifting time like startOf() may do.
|
|
||||||
setBrowsingDate(
|
|
||||||
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
|
||||||
);
|
|
||||||
if (date) {
|
|
||||||
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
|
||||||
setSelectedDate(dayjs.tz(date, timeZone));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [router.isReady, month, date, duration, timeZone]);
|
|
||||||
|
|
||||||
const { i18n, isLocaleReady } = useLocale();
|
|
||||||
const { slots: _1 } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime: selectedDate?.startOf("day"),
|
|
||||||
endTime: selectedDate?.endOf("day"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
const { slots: _2, isLoading } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime:
|
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DatePicker
|
|
||||||
isLoading={isLoading}
|
|
||||||
className={classNames(
|
|
||||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 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"
|
|
||||||
)}
|
|
||||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
|
||||||
selected={selectedDate}
|
|
||||||
onChange={(newDate) => {
|
|
||||||
setDate(newDate.format("YYYY-MM-DD"));
|
|
||||||
}}
|
|
||||||
onMonthChange={(newMonth) => {
|
|
||||||
setMonth(newMonth.format("YYYY-MM"));
|
|
||||||
}}
|
|
||||||
browsingDate={browsingDate}
|
|
||||||
weekStart={weekStart}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref={slotPickerRef}>
|
|
||||||
<AvailableTimes
|
|
||||||
isLoading={isLoading}
|
|
||||||
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
|
|
||||||
date={selectedDate}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onTimeFormatChange={onTimeFormatChange}
|
|
||||||
eventTypeId={eventType.id}
|
|
||||||
eventTypeSlug={eventType.slug}
|
|
||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
|
||||||
recurringCount={recurringEventCount}
|
|
||||||
ethSignature={ethSignature}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TimezoneDropdown({
|
|
||||||
onChangeTimeZone,
|
|
||||||
timeZone,
|
|
||||||
}: {
|
|
||||||
onChangeTimeZone: (newTimeZone: string) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
}) {
|
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSelectTimeZone = (newTimeZone: string) => {
|
|
||||||
onChangeTimeZone(newTimeZone);
|
|
||||||
localStorageTimeZone(newTimeZone);
|
|
||||||
setIsTimeOptionsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
|
||||||
<p className="flex items-center text-sm font-medium">
|
|
||||||
<Icon.FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
|
|
||||||
{timeZone}
|
|
||||||
{isTimeOptionsOpen ? (
|
|
||||||
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
) : (
|
|
||||||
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Portal>
|
|
||||||
<Popover.Content
|
|
||||||
hideWhenDetached
|
|
||||||
align="start"
|
|
||||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Portal>
|
|
||||||
</Popover.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateQuerySchema = z.object({
|
const dateQuerySchema = z.object({
|
||||||
rescheduleUid: z.string().optional().default(""),
|
rescheduleUid: z.string().optional().default(""),
|
||||||
@@ -302,7 +109,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
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.
|
||||||
@@ -353,10 +160,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isBackgroundTransparent
|
isBackgroundTransparent
|
||||||
? ""
|
? ""
|
||||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
||||||
"border-bookinglightest overflow-hidden md:rounded-md md:border",
|
"border-bookinglightest md:rounded-md md:border",
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="md:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -367,7 +174,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="flex items-start text-sm font-medium">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<Icon.FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<FiRefreshCcw 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 })}
|
||||||
@@ -390,14 +197,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{stripeAppData.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">
|
||||||
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={paymentAppData.price / 100.0}
|
||||||
style="currency"
|
style="currency"
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,26 +1,30 @@
|
|||||||
import { ErrorMessage } from "@hookform/error-message";
|
import { ErrorMessage } from "@hookform/error-message";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch, useFormContext } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
getEventLocationType,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
getMessageForOrganizer,
|
getMessageForOrganizer,
|
||||||
LocationObject,
|
|
||||||
LocationType,
|
LocationType,
|
||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
|
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
|
import type { LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||||
|
|
||||||
@@ -45,16 +49,19 @@ const LocationInput = (props: {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
||||||
|
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 (
|
||||||
<PhoneInput
|
<Controller
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
control={locationFormMethods.control}
|
control={control}
|
||||||
{...remainingProps}
|
render={({ field: { onChange, value } }) => {
|
||||||
|
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -184,6 +191,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
|
data-testid="display-location"
|
||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||||
description={t("display_location_label")}
|
description={t("display_location_label")}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -204,10 +212,10 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
||||||
<DialogContent disableOverflow>
|
<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-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
<FiMapPin className="text-primary-600 h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
@@ -215,7 +223,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
{t("edit_location")}
|
{t("edit_location")}
|
||||||
</h3>
|
</h3>
|
||||||
{!booking && (
|
{!booking && (
|
||||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
<p className="text-sm text-gray-400">
|
||||||
|
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
||||||
|
Can't find the right video app? Visit our
|
||||||
|
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
||||||
|
App Store
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
||||||
@@ -288,30 +304,31 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
name="locationType"
|
name="locationType"
|
||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<LocationSelect
|
<div className="py-4">
|
||||||
maxMenuHeight={300}
|
<LocationSelect
|
||||||
name="location"
|
maxMenuHeight={300}
|
||||||
defaultValue={selection}
|
name="location"
|
||||||
options={locationOptions}
|
defaultValue={selection}
|
||||||
isSearchable
|
options={locationOptions}
|
||||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
isSearchable
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
locationFormMethods.setValue("locationType", val.value);
|
||||||
locationFormMethods.unregister([
|
locationFormMethods.unregister([
|
||||||
"locationLink",
|
"locationLink",
|
||||||
"locationAddress",
|
"locationAddress",
|
||||||
"locationPhoneNumber",
|
"locationPhoneNumber",
|
||||||
]);
|
]);
|
||||||
locationFormMethods.clearErrors([
|
locationFormMethods.clearErrors([
|
||||||
"locationLink",
|
"locationLink",
|
||||||
"locationPhoneNumber",
|
"locationPhoneNumber",
|
||||||
"locationAddress",
|
"locationAddress",
|
||||||
]);
|
]);
|
||||||
setSelectedLocation?.(val);
|
setSelectedLocation?.(val);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -325,14 +342,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
setSelectedLocation?.(undefined);
|
setSelectedLocation?.(undefined);
|
||||||
setEditingLocationType?.("");
|
setEditingLocationType?.("");
|
||||||
locationFormMethods.unregister("locationType");
|
locationFormMethods.unregister(["locationType", "locationLink"]);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
color="secondary">
|
color="secondary">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
<Button data-testid="update-location" type="submit">
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -9,10 +10,10 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
Icon,
|
|
||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import { FiClock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -43,7 +44,7 @@ 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]">
|
||||||
<Icon.FiClock className="m-auto h-6 w-6" />
|
<FiClock 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")} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { components, OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
@@ -8,7 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
|
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
@@ -98,100 +100,108 @@ const format = (date: Date, hour12: boolean) =>
|
|||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
const EventTypeScheduleDetails = () => {
|
||||||
|
const { data: loggedInUser } = useMeQuery();
|
||||||
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
|
||||||
const me = useMeQuery();
|
|
||||||
const timeFormat = me?.data?.timeFormat;
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="min-w-4 mb-2">
|
|
||||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
|
||||||
{t("availability")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => (
|
|
||||||
<AvailabilitySelect
|
|
||||||
value={field.value}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
onChange={(selected) => {
|
|
||||||
field.onChange(selected?.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
|
||||||
<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">
|
|
||||||
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
href={`/availability/${schedule?.schedule.id}`}
|
|
||||||
color="minimal"
|
|
||||||
EndIcon={Icon.FiExternalLink}
|
|
||||||
target="_blank"
|
|
||||||
className="justify-center border sm:border-0"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{t("edit_availability")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
|
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||||
|
);
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
const filterDays = (dayNum: number) =>
|
||||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
if (!isTeamEvent) {
|
return (
|
||||||
return <EventTypeSchedule />;
|
<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 (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||||
@@ -200,6 +210,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
checked={!value}
|
checked={!value}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
onChange(!checked);
|
onChange(!checked);
|
||||||
|
if (checked) {
|
||||||
|
resetField("schedule");
|
||||||
|
} else {
|
||||||
|
setValue("schedule", null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title={t("choose_common_schedule_team_event")}
|
title={t("choose_common_schedule_team_event")}
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
@@ -209,3 +224,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||||
|
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import type { FC } from "react";
|
||||||
|
import type { SubmitHandler } from "react-hook-form";
|
||||||
|
import { FormProvider } from "react-hook-form";
|
||||||
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
|
import { getEventName } from "@calcom/core/event";
|
||||||
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, Dialog, DialogClose, DialogFooter, DialogContent, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
customEventName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomEventTypeModalFormProps {
|
||||||
|
placeHolder: string;
|
||||||
|
close: () => void;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
event: EventNameObjectType;
|
||||||
|
defaultValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { placeHolder, close, setValue, event } = props;
|
||||||
|
const { register, handleSubmit, watch, getValues } = useFormContext<FormValues>();
|
||||||
|
const onSubmit: SubmitHandler<FormValues> = (data) => {
|
||||||
|
setValue(data.customEventName);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// const customEventName = watch("customEventName");
|
||||||
|
const previewText = getEventName({ ...event, eventName: watch("customEventName") });
|
||||||
|
const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="custom-event-name"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const isEmpty = getValues("customEventName") === "";
|
||||||
|
if (isEmpty) {
|
||||||
|
setValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(onSubmit)(e);
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
label={t("event_name_in_calendar")}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeHolder_}
|
||||||
|
{...register("customEventName", {
|
||||||
|
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
|
||||||
|
})}
|
||||||
|
className="mb-0"
|
||||||
|
/>
|
||||||
|
<div className="pt-6 text-sm">
|
||||||
|
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
||||||
|
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
||||||
|
<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-gray-900">{t("event_name_info")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2.5 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
||||||
|
<p className="text-gray-900">{t("your_full_name")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2.5 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
||||||
|
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
||||||
|
<p className="text-gray-900">{t("location_info")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
||||||
|
<div
|
||||||
|
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/calendar-preview.svg)",
|
||||||
|
}}>
|
||||||
|
<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">
|
||||||
|
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
|
||||||
|
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomEventTypeModalProps {
|
||||||
|
placeHolder: string;
|
||||||
|
defaultValue: string;
|
||||||
|
close: () => void;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
event: EventNameObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomEventTypeModal: FC<CustomEventTypeModalProps> = (props) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const { defaultValue, placeHolder, close, setValue, event } = props;
|
||||||
|
|
||||||
|
const methods = useForm<FormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
customEventName: defaultValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={close}>
|
||||||
|
<DialogContent
|
||||||
|
title={t("custom_event_name")}
|
||||||
|
description={t("custom_event_name_description")}
|
||||||
|
type="creation"
|
||||||
|
enableOverflow>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<CustomEventTypeModalForm
|
||||||
|
event={event}
|
||||||
|
close={close}
|
||||||
|
setValue={setValue}
|
||||||
|
placeHolder={placeHolder}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>{t("cancel")}</DialogClose>
|
||||||
|
|
||||||
|
<Button form="custom-event-name" type="submit" color="primary">
|
||||||
|
{t("create")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomEventTypeModal;
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|
||||||
import { EventTypeCustomInputType } from "@prisma/client";
|
|
||||||
import type { CustomInputParsed } from "pages/event-types/[type]";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
|
||||||
label: string;
|
|
||||||
value: EventTypeCustomInputType;
|
|
||||||
options?: { label: string; type: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: (output: CustomInputParsed) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
selectedCustomInput?: CustomInputParsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IFormInput = CustomInputParsed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getting a random ID gives us the option to know WHICH field is changed
|
|
||||||
* when the user edits a custom field.
|
|
||||||
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
|
|
||||||
* There is very very very slim chance that this will cause a collision
|
|
||||||
* */
|
|
||||||
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
|
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const inputOptions: OptionTypeBase[] = [
|
|
||||||
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
|
|
||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
|
||||||
{
|
|
||||||
value: EventTypeCustomInputType.RADIO,
|
|
||||||
label: t("radio"),
|
|
||||||
},
|
|
||||||
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
|
||||||
];
|
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
|
||||||
|
|
||||||
const defaultValues = selectedCustomInput
|
|
||||||
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
|
|
||||||
: {
|
|
||||||
id: randomId(),
|
|
||||||
type: EventTypeCustomInputType.TEXT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { register, control, getValues } = useForm<IFormInput>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
|
||||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("input_type")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="type"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
id="type"
|
|
||||||
defaultValue={selectedInputOption}
|
|
||||||
options={inputOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
|
||||||
onChange={(option) => option && field.onChange(option.value)}
|
|
||||||
value={selectedInputOption}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TextField
|
|
||||||
label={t("label")}
|
|
||||||
type="text"
|
|
||||||
id="label"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.label}
|
|
||||||
{...register("label", { required: true })}
|
|
||||||
/>
|
|
||||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
|
||||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
|
||||||
<TextField
|
|
||||||
label={t("placeholder")}
|
|
||||||
type="text"
|
|
||||||
id="placeholder"
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
|
||||||
{...register("placeholder")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedInputType === EventTypeCustomInputType.RADIO && (
|
|
||||||
<RadioInputHandler control={control} register={register} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="required"
|
|
||||||
type="checkbox"
|
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultChecked={selectedCustomInput?.required ?? true}
|
|
||||||
{...register("required")}
|
|
||||||
/>
|
|
||||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("is_required")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="eventTypeId"
|
|
||||||
value={selectedCustomInput?.eventTypeId || -1}
|
|
||||||
{...register("eventTypeId", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id"
|
|
||||||
value={selectedCustomInput?.id || -1}
|
|
||||||
{...register("id", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
|
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
props.onSubmit(getValues());
|
|
||||||
}}>
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function RadioInputHandler({
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
}: {
|
|
||||||
register: UseFormRegister<IFormInput>;
|
|
||||||
control: Control<IFormInput>;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { fields, append, remove } = useFieldArray<IFormInput>({
|
|
||||||
control,
|
|
||||||
name: "options",
|
|
||||||
shouldUnregister: true,
|
|
||||||
});
|
|
||||||
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col ">
|
|
||||||
<Label htmlFor="radio_options">{t("options")}</Label>
|
|
||||||
<ul
|
|
||||||
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
|
|
||||||
ref={animateRef}>
|
|
||||||
<>
|
|
||||||
{fields.map((option, index) => (
|
|
||||||
<li key={`${option.id}`}>
|
|
||||||
<TextField
|
|
||||||
id={option.id}
|
|
||||||
placeholder={t("enter_option", { index: index + 1 })}
|
|
||||||
addOnFilled={false}
|
|
||||||
label={t("option", { index: index + 1 })}
|
|
||||||
labelSrOnly
|
|
||||||
{...register(`options.${index}.label` as const, { required: true })}
|
|
||||||
addOnSuffix={
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
color="minimal"
|
|
||||||
StartIcon={Icon.FiX}
|
|
||||||
onClick={() => {
|
|
||||||
remove(index);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
color="minimal"
|
|
||||||
StartIcon={Icon.FiPlus}
|
|
||||||
className="!text-sm !font-medium"
|
|
||||||
onClick={() => {
|
|
||||||
append({ label: "", type: "text" });
|
|
||||||
}}>
|
|
||||||
{t("add_an_option")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
|
||||||
@@ -1,35 +1,25 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
|
import { getEventName } from "@calcom/core/event";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||||
Badge,
|
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
Icon,
|
|
||||||
Label,
|
|
||||||
SettingsToggle,
|
|
||||||
showToast,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
|
|
||||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
|
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||||
|
|
||||||
const generateHashedLink = (id: number) => {
|
const generateHashedLink = (id: number) => {
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const seed = `${id}:${new Date().getTime()}`;
|
const seed = `${id}:${new Date().getTime()}`;
|
||||||
@@ -37,51 +27,55 @@ const generateHashedLink = (id: number) => {
|
|||||||
return uid;
|
return uid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomId = (length = 8) => {
|
|
||||||
return (
|
|
||||||
-1 *
|
|
||||||
parseInt(
|
|
||||||
Math.ceil(Math.random() * Date.now())
|
|
||||||
.toPrecision(length)
|
|
||||||
.toString()
|
|
||||||
.replace(".", "")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
const eventNameObject: EventNameObjectType = {
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
attendeeName: t("scheduler"),
|
||||||
);
|
eventType: eventType.title,
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
|
eventName: eventType.eventName,
|
||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
host: eventType.users[0]?.name || "Nameless",
|
||||||
|
t,
|
||||||
|
};
|
||||||
|
|
||||||
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.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
|
||||||
formMethods.getValues("customInputs").splice(index, 1);
|
|
||||||
customInputs.splice(index, 1);
|
|
||||||
setCustomInputs([...customInputs]);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
}, [eventType.users, hashedUrl, team?.id]);
|
}, [eventType.users, hashedUrl, team?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleGuests = (enabled: boolean) => {
|
||||||
if (eventType.customInputs) {
|
const bookingFields = formMethods.getValues("bookingFields");
|
||||||
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
|
formMethods.setValue(
|
||||||
}
|
"bookingFields",
|
||||||
}, [eventType.customInputs]);
|
bookingFields.map((field) => {
|
||||||
|
if (field.name === "guests") {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
hidden: !enabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventNamePlaceholder = getEventName({
|
||||||
|
...eventNameObject,
|
||||||
|
eventName: formMethods.watch("eventName"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
|
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-8">
|
<div className="flex flex-col space-y-8">
|
||||||
{/**
|
{/**
|
||||||
@@ -120,66 +114,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={Icon.FiEdit}
|
StartIcon={FiEdit}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
onClick={() => setShowEventNameTip((old) => !old)}
|
||||||
|
aria-label="edit custom name"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="">
|
<FormBuilder
|
||||||
<SettingsToggle
|
title={t("booking_questions_title")}
|
||||||
title={t("additional_inputs")}
|
description={t("booking_questions_description")}
|
||||||
description={t("additional_input_description")}
|
addFieldLabel={t("add_a_booking_question")}
|
||||||
checked={customInputs.length > 0}
|
formProp="bookingFields"
|
||||||
onCheckedChange={(e) => {
|
/>
|
||||||
if (e && customInputs.length === 0) {
|
|
||||||
// Push a placeholders
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
} else if (!e) {
|
|
||||||
formMethods.setValue("customInputs", []);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<ul className="my-4 rounded-md border">
|
|
||||||
{customInputs.map((customInput, idx) => (
|
|
||||||
<CustomInputItem
|
|
||||||
key={idx}
|
|
||||||
question={customInput.label}
|
|
||||||
type={customInput.type}
|
|
||||||
required={customInput.required}
|
|
||||||
editOnClick={() => {
|
|
||||||
setSelectedCustomInput(customInput);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
}}
|
|
||||||
deleteOnClick={() => removeCustom(idx)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{customInputs.length > 0 && (
|
|
||||||
<Button
|
|
||||||
StartIcon={Icon.FiPlus}
|
|
||||||
color="minimal"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
}}>
|
|
||||||
{t("add_input")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</SettingsToggle>
|
|
||||||
</div>
|
|
||||||
<hr />
|
<hr />
|
||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
@@ -187,22 +146,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
onRequiresConfirmation={setRequiresConfirmation}
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
/>
|
/>
|
||||||
<hr />
|
|
||||||
<Controller
|
|
||||||
name="disableGuests"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.disableGuests}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SettingsToggle
|
|
||||||
title={t("disable_guests")}
|
|
||||||
description={t("disable_guests_description")}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
disabled={seatsEnabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
name="hideCalendarNotes"
|
name="hideCalendarNotes"
|
||||||
@@ -218,22 +161,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
|
||||||
name="metadata.additionalNotesRequired"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={!!eventType.metadata.additionalNotesRequired}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="flex space-x-3 ">
|
|
||||||
<SettingsToggle
|
|
||||||
title={t("require_additional_notes")}
|
|
||||||
description={t("require_additional_notes_description")}
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -314,7 +241,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
}}
|
}}
|
||||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||||
type="button">
|
type="button">
|
||||||
<Icon.FiCopy />
|
<FiCopy />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
@@ -334,13 +261,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
if (e) {
|
if (e) {
|
||||||
formMethods.setValue("disableGuests", true);
|
toggleGuests(false);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
setRequiresConfirmation(false);
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
formMethods.setValue("disableGuests", false);
|
toggleGuests(true);
|
||||||
}
|
}
|
||||||
onChange(e);
|
onChange(e);
|
||||||
}}>
|
}}>
|
||||||
@@ -378,89 +305,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
<CustomEventTypeModal
|
||||||
<DialogContent
|
close={closeEventNameTip}
|
||||||
title={t("custom_event_name")}
|
setValue={setEventName}
|
||||||
description={t("custom_event_name_description")}
|
defaultValue={eventType.eventName || ""}
|
||||||
type="creation">
|
placeHolder={eventNamePlaceholder}
|
||||||
<TextField
|
event={eventNameObject}
|
||||||
label={t("event_name")}
|
/>
|
||||||
type="text"
|
|
||||||
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
|
||||||
defaultValue={eventType.eventName || ""}
|
|
||||||
{...formMethods.register("eventName")}
|
|
||||||
/>
|
|
||||||
<div className="mt-1 text-gray-500">
|
|
||||||
<p>{`{HOST} = ${t("your_name")}`}</p>
|
|
||||||
<p>{`{ATTENDEE} = ${t("attendee_name")}`}</p>
|
|
||||||
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
|
||||||
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
|
|
||||||
{t("create")}
|
|
||||||
</Button>
|
|
||||||
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
)}
|
||||||
<Controller
|
|
||||||
name="customInputs"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={customInputs}
|
|
||||||
render={() => (
|
|
||||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
|
||||||
<DialogContent
|
|
||||||
type="creation"
|
|
||||||
Icon={Icon.FiPlus}
|
|
||||||
title={t("add_new_custom_input_field")}
|
|
||||||
description={t("this_input_will_shown_booking_this_event")}>
|
|
||||||
<CustomInputTypeForm
|
|
||||||
selectedCustomInput={selectedCustomInput}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
const customInput: CustomInputParsed = {
|
|
||||||
id: getRandomId(),
|
|
||||||
eventTypeId: -1,
|
|
||||||
label: values.label,
|
|
||||||
placeholder: values.placeholder,
|
|
||||||
required: values.required,
|
|
||||||
type: values.type,
|
|
||||||
options: values.options,
|
|
||||||
hasToBeCreated: true,
|
|
||||||
};
|
|
||||||
if (selectedCustomInput) {
|
|
||||||
selectedCustomInput.label = customInput.label;
|
|
||||||
selectedCustomInput.placeholder = customInput.placeholder;
|
|
||||||
selectedCustomInput.required = customInput.required;
|
|
||||||
selectedCustomInput.type = customInput.type;
|
|
||||||
selectedCustomInput.options = customInput.options || undefined;
|
|
||||||
selectedCustomInput.hasToBeCreated = false;
|
|
||||||
// Update by id
|
|
||||||
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
|
||||||
customInputs[inputIndex] = selectedCustomInput;
|
|
||||||
setCustomInputs(customInputs);
|
|
||||||
formMethods.setValue("customInputs", customInputs);
|
|
||||||
} else {
|
|
||||||
const concatted = customInputs.concat({
|
|
||||||
...customInput,
|
|
||||||
options: customInput.options,
|
|
||||||
});
|
|
||||||
console.log(concatted);
|
|
||||||
setCustomInputs(concatted);
|
|
||||||
formMethods.setValue("customInputs", concatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
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, Icon } from "@calcom/ui";
|
import { Button, EmptyScreen } from "@calcom/ui";
|
||||||
|
import { FiGrid } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
@@ -60,7 +61,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{!isLoading && !installedApps?.length ? (
|
{!isLoading && !installedApps?.length ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Icon.FiGrid}
|
Icon={FiGrid}
|
||||||
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={
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||||
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
|
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const MinimumBookingNoticeInput = React.forwardRef<
|
const MinimumBookingNoticeInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
@@ -419,19 +422,21 @@ const BookingLimits = () => {
|
|||||||
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const current = currentBookingLimits;
|
const current = currentBookingLimits;
|
||||||
|
const currentValue = watchBookingLimits[bookingLimitKey];
|
||||||
|
|
||||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||||
delete current[bookingLimitKey];
|
delete current[bookingLimitKey];
|
||||||
const newData = {
|
const newData = {
|
||||||
...current,
|
...current,
|
||||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||||
[val?.value as BookingLimitsKey]: watchBookingLimits[bookingLimitKey],
|
[val?.value as BookingLimitsKey]: currentValue,
|
||||||
};
|
};
|
||||||
onChange(newData);
|
onChange(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={FiTrash}
|
||||||
color="destructive"
|
color="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = currentBookingLimits;
|
const current = currentBookingLimits;
|
||||||
@@ -445,7 +450,7 @@ const BookingLimits = () => {
|
|||||||
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
StartIcon={Icon.FiPlus}
|
StartIcon={FiPlus}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!currentBookingLimits || !watchBookingLimits) return;
|
if (!currentBookingLimits || !watchBookingLimits) return;
|
||||||
const currentKeys = Object.keys(watchBookingLimits);
|
const currentKeys = Object.keys(watchBookingLimits);
|
||||||
@@ -458,7 +463,7 @@ const BookingLimits = () => {
|
|||||||
|
|
||||||
setValue("bookingLimits", {
|
setValue("bookingLimits", {
|
||||||
...watchBookingLimits,
|
...watchBookingLimits,
|
||||||
[rest[0].value]: undefined,
|
[rest[0].value]: 1,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
{t("add_limit")}
|
{t("add_limit")}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
|
||||||
import RecurringEventController from "./RecurringEventController";
|
import RecurringEventController from "./RecurringEventController";
|
||||||
|
|
||||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
const requirePayment = stripeAppData.price > 0;
|
const requirePayment = paymentAppData.price > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ 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 { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import { slugify } from "@lib/slugify";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
|
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||||
|
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import LocationSelect, {
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
SingleValueLocationOption,
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
LocationOption,
|
|
||||||
} from "@components/ui/form/LocationSelect";
|
|
||||||
|
|
||||||
const getLocationFromType = (
|
const getLocationFromType = (
|
||||||
type: EventLocationType["type"],
|
type: EventLocationType["type"],
|
||||||
@@ -34,12 +35,25 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||||
|
for (const locationType of options) {
|
||||||
|
for (const location of locationType.options) {
|
||||||
|
if (location.value === type && location.disabled === false) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
props: Pick<
|
||||||
|
EventTypeSetupProps,
|
||||||
|
"eventType" | "locationOptions" | "team" | "teamMembers" | "destinationCalendar"
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team } = props;
|
const { eventType, locationOptions, 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);
|
||||||
@@ -66,10 +80,15 @@ export const EventSetupTab = (
|
|||||||
setShowLocationModal(true);
|
setShowLocationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type),
|
formMethods.getValues("locations").filter((location) => {
|
||||||
|
if (location.type === LocationType.InPerson) {
|
||||||
|
return location.address !== selectedLocation.address;
|
||||||
|
}
|
||||||
|
return location.type !== selectedLocation.type;
|
||||||
|
}),
|
||||||
{ shouldValidate: true }
|
{ shouldValidate: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -84,14 +103,14 @@ export const EventSetupTab = (
|
|||||||
...details,
|
...details,
|
||||||
type: newLocationType,
|
type: newLocationType,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
copy[existingIdx] = {
|
|
||||||
...formMethods.getValues("locations")[existingIdx],
|
|
||||||
...details,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formMethods.setValue("locations", copy);
|
formMethods.setValue("locations", [
|
||||||
|
...copy,
|
||||||
|
...(newLocationType === LocationType.InPerson && editingLocationType === ""
|
||||||
|
? [{ ...details, type: newLocationType }]
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
@@ -147,6 +166,7 @@ export const EventSetupTab = (
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
|
menuPlacement="auto"
|
||||||
onChange={(e: SingleValueLocationOption) => {
|
onChange={(e: SingleValueLocationOption) => {
|
||||||
if (e?.value) {
|
if (e?.value) {
|
||||||
const newLocationType = e.value;
|
const newLocationType = e.value;
|
||||||
@@ -172,18 +192,24 @@ export const EventSetupTab = (
|
|||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// We dont want to translate the string link - it doesnt exist in common.json and it gets prefixed/suffixed with __ or //
|
||||||
|
const eventLabel =
|
||||||
|
eventLocationType.defaultValueVariable === "link"
|
||||||
|
? eventLocationType.label
|
||||||
|
: t(location[eventLocationType.defaultValueVariable] || eventLocationType.label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
<li
|
||||||
<div className="flex max-w-full justify-between">
|
key={`${location.type}${index}`}
|
||||||
<div key={index} className="flex flex-grow items-center">
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
||||||
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -198,19 +224,22 @@ export const EventSetupTab = (
|
|||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
||||||
<Icon.FiEdit2 className="h-4 w-4" />
|
<FiEdit2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||||
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{validLocations.some((location) => location.type === MeetLocationType) && (
|
{validLocations.some(
|
||||||
|
(location) =>
|
||||||
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
|
) && (
|
||||||
<div className="flex text-sm text-gray-600">
|
<div className="flex text-sm text-gray-600">
|
||||||
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<FiCheck 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.
|
||||||
@@ -220,14 +249,17 @@ export const EventSetupTab = (
|
|||||||
className="underline">
|
className="underline">
|
||||||
here.
|
here.
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
We will fall back to Cal video if you do not change it.
|
|
||||||
</p>
|
</p>
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
{validLocations.length > 0 && (
|
||||||
<li>
|
<li>
|
||||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
<Button
|
||||||
|
data-testid="add-location"
|
||||||
|
StartIcon={FiPlus}
|
||||||
|
color="minimal"
|
||||||
|
onClick={() => setShowLocationModal(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@@ -247,12 +279,15 @@ export const EventSetupTab = (
|
|||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<div>
|
||||||
label={t("description")}
|
<Label>{t("description")}</Label>
|
||||||
placeholder={t("quick_video_meeting")}
|
<Editor
|
||||||
defaultValue={eventType.description ?? ""}
|
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
||||||
{...formMethods.register("description")}
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
/>
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Options } from "react-select";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import type { Options } from "react-select";
|
||||||
|
|
||||||
import CheckedTeamSelect, {
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
CheckedSelectOption,
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
|
||||||
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";
|
||||||
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
|
if (a.label < b.label) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.label > b.label) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const FixedHosts = ({
|
const FixedHosts = ({
|
||||||
control,
|
control,
|
||||||
labelText,
|
labelText,
|
||||||
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
];
|
];
|
||||||
|
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && (
|
||||||
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
|
|
||||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
placeholder={t("add_attendees")}
|
placeholder={t("add_attendees")}
|
||||||
labelText={t("team")}
|
labelText={t("team")}
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||||
<>
|
<>
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
)
|
)
|
||||||
.filter(Boolean)}
|
.filter(Boolean)}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
options={teamMembers.map(mapUserToValue)}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods
|
!!formMethods
|
||||||
.getValues("hostsFixed")
|
.getValues("hostsFixed")
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { EventTypeSetupProps, FormValues } 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 { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import { 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 { Webhook } from "@calcom/prisma/client";
|
import type { Webhook } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const EventTeamWebhooksTab = ({
|
export const EventTeamWebhooksTab = ({
|
||||||
eventType,
|
eventType,
|
||||||
@@ -84,7 +85,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
data-testid="new_webhook"
|
data-testid="new_webhook"
|
||||||
StartIcon={Icon.FiPlus}
|
StartIcon={FiPlus}
|
||||||
onClick={() => setCreateModalOpen(true)}>
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
{t("new_webhook")}
|
{t("new_webhook")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -99,7 +100,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<>
|
<>
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 mb-8 rounded-md border">
|
<div className="mb-8 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
@@ -130,7 +131,10 @@ export const EventTeamWebhooksTab = ({
|
|||||||
|
|
||||||
{/* New webhook dialog */}
|
{/* New webhook dialog */}
|
||||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||||
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
|
<DialogContent
|
||||||
|
enableOverflow
|
||||||
|
title={t("create_webhook")}
|
||||||
|
description={t("create_webhook_team_event_type")}>
|
||||||
<WebhookForm
|
<WebhookForm
|
||||||
onSubmit={onCreateWebhook}
|
onSubmit={onCreateWebhook}
|
||||||
onCancel={() => setCreateModalOpen(false)}
|
onCancel={() => setCreateModalOpen(false)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { 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 { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
Icon,
|
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
@@ -32,6 +31,21 @@ import {
|
|||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import {
|
||||||
|
FiLink,
|
||||||
|
FiCalendar,
|
||||||
|
FiClock,
|
||||||
|
FiSliders,
|
||||||
|
FiRepeat,
|
||||||
|
FiGrid,
|
||||||
|
FiZap,
|
||||||
|
FiUsers,
|
||||||
|
FiExternalLink,
|
||||||
|
FiCode,
|
||||||
|
FiTrash,
|
||||||
|
FiMoreHorizontal,
|
||||||
|
FiLoader,
|
||||||
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
|
||||||
@@ -63,44 +77,44 @@ 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: Icon.FiLink,
|
icon: FiLink,
|
||||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
icon: Icon.FiCalendar,
|
icon: FiCalendar,
|
||||||
info: `default_schedule_name`, // TODO: Get this from props
|
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: Icon.FiClock,
|
icon: FiClock,
|
||||||
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: Icon.FiSliders,
|
icon: FiSliders,
|
||||||
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: Icon.FiRepeat,
|
icon: FiRepeat,
|
||||||
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: Icon.FiGrid,
|
icon: FiGrid,
|
||||||
//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: Icon.FiZap,
|
icon: FiZap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -157,7 +171,7 @@ function EventTypeSingleLayout({
|
|||||||
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: Icon.FiUsers,
|
icon: FiUsers,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||||
});
|
});
|
||||||
navigation.push({
|
navigation.push({
|
||||||
@@ -181,10 +195,9 @@ function EventTypeSingleLayout({
|
|||||||
backPath="/event-types"
|
backPath="/event-types"
|
||||||
title={eventType.title + " | " + t("event_type")}
|
title={eventType.title + " | " + t("event_type")}
|
||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
subtitle={eventType.description || ""}
|
|
||||||
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:flex sm:hover:bg-gray-100">
|
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
@@ -193,7 +206,7 @@ function EventTypeSingleLayout({
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
defaultChecked={formMethods.getValues("hidden")}
|
checked={formMethods.watch("hidden")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hidden", e);
|
formMethods.setValue("hidden", e);
|
||||||
}}
|
}}
|
||||||
@@ -211,14 +224,14 @@ function EventTypeSingleLayout({
|
|||||||
variant="icon"
|
variant="icon"
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
StartIcon={Icon.FiExternalLink}
|
StartIcon={FiExternalLink}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiLink}
|
StartIcon={FiLink}
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
@@ -227,7 +240,7 @@ function EventTypeSingleLayout({
|
|||||||
/>
|
/>
|
||||||
<EmbedButton
|
<EmbedButton
|
||||||
embedUrl={encodeURIComponent(embedLink)}
|
embedUrl={encodeURIComponent(embedLink)}
|
||||||
StartIcon={Icon.FiCode}
|
StartIcon={FiCode}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
@@ -235,7 +248,7 @@ function EventTypeSingleLayout({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={FiTrash}
|
||||||
tooltip={t("delete")}
|
tooltip={t("delete")}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
@@ -246,19 +259,14 @@ function EventTypeSingleLayout({
|
|||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||||
className="lg:hidden"
|
|
||||||
StartIcon={Icon.FiMoreHorizontal}
|
|
||||||
variant="icon"
|
|
||||||
color="secondary"
|
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={Icon.FiExternalLink}
|
StartIcon={FiExternalLink}
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
@@ -267,7 +275,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={Icon.FiLink}
|
StartIcon={FiLink}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
@@ -278,23 +286,24 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={Icon.FiTrash}
|
color="destructive"
|
||||||
|
StartIcon={FiTrash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="block sm:hidden" />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
className="mt-2 inline cursor-pointer self-center pr-2 sm:hidden">
|
className="mt-2 inline cursor-pointer self-center pr-2 ">
|
||||||
{t("hide_from_profile")}
|
{t("hide_from_profile")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
defaultChecked={formMethods.getValues("hidden")}
|
checked={formMethods.watch("hidden")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hidden", e);
|
formMethods.setValue("hidden", e);
|
||||||
}}
|
}}
|
||||||
@@ -313,8 +322,8 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Suspense fallback={<Icon.FiLoader />}>
|
<Suspense fallback={<FiLoader />}>
|
||||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
className="primary-navigation"
|
className="primary-navigation"
|
||||||
@@ -330,7 +339,7 @@ function EventTypeSingleLayout({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { 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 { FormValues } from "pages/event-types/[type]";
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Icon, SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
|
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -31,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">
|
||||||
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import { CalendarSwitch } from "./CalendarSwitch";
|
import { CalendarSwitch } from "./CalendarSwitch";
|
||||||
|
|
||||||
interface IConnectedCalendarItem {
|
interface IConnectedCalendarItem {
|
||||||
@@ -21,7 +19,7 @@ interface IConnectedCalendarItem {
|
|||||||
|
|
||||||
const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||||
const { name, logo, externalId, calendars, integrationType } = prop;
|
const { name, logo, externalId, calendars, integrationType } = prop;
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row items-center p-4">
|
<div className="flex flex-row items-center p-4">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterInputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { useForm } from "react-hook-form";
|
|||||||
import { Schedule } from "@calcom/features/schedules";
|
import { Schedule } from "@calcom/features/schedules";
|
||||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Form } from "@calcom/ui";
|
import { Button, Form } from "@calcom/ui";
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FormEvent, useRef, useState } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
@@ -21,18 +25,16 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const { user } = props;
|
const { user } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||||
const bioRef = useRef<HTMLTextAreaElement>(null);
|
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||||
const {
|
defaultValues: { bio: user?.bio || "" },
|
||||||
register,
|
});
|
||||||
setValue,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<FormData>({ defaultValues: { bio: user?.bio || "" } });
|
|
||||||
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
|
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
|
||||||
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: async (_data, context) => {
|
onSuccess: async (_data, context) => {
|
||||||
@@ -63,6 +65,8 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const onSubmit = handleSubmit((data: { bio: string }) => {
|
const onSubmit = handleSubmit((data: { bio: string }) => {
|
||||||
const { bio } = data;
|
const { bio } = data;
|
||||||
|
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingFinished);
|
||||||
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
bio,
|
bio,
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
@@ -138,25 +142,12 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset className="mt-8">
|
<fieldset className="mt-8">
|
||||||
<label htmlFor="bio" className="mb-2 block text-sm font-medium text-gray-700">
|
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
|
||||||
{t("about")}
|
<Editor
|
||||||
</label>
|
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||||
<TextArea
|
setText={(value: string) => setValue("bio", turndown(value))}
|
||||||
{...register("bio", { required: true })}
|
excludedToolbarItems={["blockType"]}
|
||||||
ref={bioRef}
|
|
||||||
name="bio"
|
|
||||||
id="bio"
|
|
||||||
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
|
|
||||||
defaultValue={user?.bio || undefined}
|
|
||||||
onChange={(event) => {
|
|
||||||
setValue("bio", event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{errors.bio && (
|
|
||||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
|
||||||
{t("required")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const { user, nextStep } = props;
|
const { user, nextStep } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
|
const telemetry = useTelemetry();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -31,6 +32,11 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||||
|
}, [telemetry]);
|
||||||
|
|
||||||
const defaultOptions = { required: true, maxLength: 255 };
|
const defaultOptions = { required: true, maxLength: 255 };
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SyntheticEvent, useState } from "react";
|
import type { SyntheticEvent } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge, Button } from "@calcom/ui";
|
import { Badge, Button } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Icon } from "@calcom/ui";
|
import { FiShield } 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">
|
||||||
<Icon.ShieldCheckIcon className="h-6 w-6 text-white" />
|
<FiShield className="h-6 w-6 text-white" />
|
||||||
</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-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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, Icon, showToast, Switch } from "@calcom/ui";
|
import { Badge, showToast, Switch } from "@calcom/ui";
|
||||||
|
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
export function CalendarSwitch(props: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -74,7 +75,7 @@ export function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{props.defaultSelected && (
|
{props.defaultSelected && (
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<Icon.FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
+30
-10
@@ -1,17 +1,39 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import React from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
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, Label, PasswordField, TextField } from "@calcom/ui";
|
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
|
import { FiUserCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||||
const router = useRouter();
|
const { t } = useLocale();
|
||||||
|
if (props.userCount > 0)
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="wizard-step-1"
|
||||||
|
name="wizard-step-1"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onSuccess();
|
||||||
|
}}>
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={FiUserCheck}
|
||||||
|
headline={t("admin_user_created")}
|
||||||
|
description={t("admin_user_created_description")}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
return <AdminUser {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
props.setIsLoading(false);
|
props.onError();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||||
props.setIsLoading(true);
|
props.onSubmit();
|
||||||
const response = await fetch("/api/auth/setup", {
|
const response = await fetch("/api/auth/setup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
email: data.email_address.toLowerCase(),
|
email: data.email_address.toLowerCase(),
|
||||||
password: data.password,
|
password: data.password,
|
||||||
});
|
});
|
||||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
props.onSuccess();
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
props.onError();
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SetupFormStep1;
|
|
||||||
Loaded 100 of 939 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user