Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9441da9d4e | ||
|
|
1510740cb2 | ||
|
|
8da68cc1fe | ||
|
|
6dbc8dd820 | ||
|
|
4ada428bcb | ||
|
|
6adaf0d3e8 | ||
|
|
c9e6acfa69 | ||
|
|
2272da5ee7 | ||
|
|
79b3875af3 | ||
|
|
d16b6fce8f | ||
|
|
40b35bdbe4 | ||
|
|
0af76ec5ab | ||
|
|
f7b20da640 | ||
|
|
eedf6913b8 | ||
|
|
78393a2b8b | ||
|
|
b3d64104b0 | ||
|
|
9266ba1e48 | ||
|
|
ec212e4317 | ||
|
|
02d4f0542a | ||
|
|
0aae07ae2f | ||
|
|
80ab6c9c98 | ||
|
|
86ba0766fc | ||
|
|
0adde17f38 | ||
|
|
9e4700f9df | ||
|
|
f4a87c66b3 | ||
|
|
0c06f1f6cb | ||
|
|
b731dfd421 | ||
|
|
0b704b9ed4 | ||
|
|
a0ece0d77e | ||
|
|
7839381911 | ||
|
|
6a976f5b42 | ||
|
|
8718905474 | ||
|
|
ff968d3867 | ||
|
|
8f5e6370b0 | ||
|
|
0a0c400333 | ||
|
|
0eb05aa540 | ||
|
|
efe18ebdcd | ||
|
|
ce097b89ba | ||
|
|
bb0e6389fa | ||
|
|
19a7cd1a50 | ||
|
|
49bcd5fec7 | ||
|
|
000a6e0d35 | ||
|
|
52936f60b4 | ||
|
|
2d6eebcd8a | ||
|
|
cfa6a5b996 | ||
|
|
7413ba6aa0 | ||
|
|
e9cbe5d353 | ||
|
|
d8bc9acbbc | ||
|
|
4f0e843ac6 | ||
|
|
02f1c50bb2 | ||
|
|
5952a38ab9 | ||
|
|
e5fb19163e | ||
|
|
79f34a4fc7 | ||
|
|
3e90c08d55 | ||
|
|
697d8eca43 | ||
|
|
5cecf3104f | ||
|
|
a3a6caa6dd | ||
|
|
d3586c9039 | ||
|
|
71eafa287a | ||
|
|
828ef8580e | ||
|
|
a348c3a20a | ||
|
|
3c68c7f7cb | ||
|
|
820b2da16d | ||
|
|
6bdb2b94eb | ||
|
|
3c3db0b3a6 | ||
|
|
4748641c22 | ||
|
|
804787f2a3 | ||
|
|
b7c3b31e0c | ||
|
|
ab6781cd72 | ||
|
|
b070cfb227 | ||
|
|
5acb3a09e5 | ||
|
|
69c9f690cb | ||
|
|
d2a12ea834 | ||
|
|
16f24b4831 | ||
|
|
a3eeeab259 | ||
|
|
f69c254fbc | ||
|
|
f0f6bd4846 | ||
|
|
8816a18854 | ||
|
|
4e529e5c6c | ||
|
|
e521bf6f0b | ||
|
|
a91a0f86e0 | ||
|
|
f654003f95 | ||
|
|
4cc8a6fbf4 | ||
|
|
5167608404 | ||
|
|
9c93685431 | ||
|
|
a380087c9b | ||
|
|
3559e0d082 | ||
|
|
4323b0c2d1 | ||
|
|
3a800f7511 | ||
|
|
c203e5a38b | ||
|
|
3f5b3f06a2 | ||
|
|
d52635efbb | ||
|
|
4add4df08e | ||
|
|
df6d7e8df7 | ||
|
|
dc61640118 | ||
|
|
361108246e | ||
|
|
a5e25b9b28 | ||
|
|
ccff528765 | ||
|
|
24a1ef6721 | ||
|
|
b89919129e | ||
|
|
cbf3edefc7 | ||
|
|
909ae5f2fb | ||
|
|
0bbe5173dc | ||
|
|
84ad133077 | ||
|
|
43647a797f | ||
|
|
f469a9c127 | ||
|
|
212f7a374f | ||
|
|
1929d5c7d4 | ||
|
|
8ae54cd7cc | ||
|
|
bb13009e64 | ||
|
|
648dd862f5 | ||
|
|
8eccd3658e | ||
|
|
ed65b2a3ab | ||
|
|
0930121680 | ||
|
|
1068873f27 | ||
|
|
a1ece0be63 | ||
|
|
c550b4c8fc | ||
|
|
e95650637c | ||
|
|
0f3e0a1554 | ||
|
|
761dfac033 | ||
|
|
9fda4acc5b | ||
|
|
0470708aad | ||
|
|
bee392ba1a | ||
|
|
448f57442b | ||
|
|
856b090b4b | ||
|
|
b99661a411 | ||
|
|
d96a26eb3c | ||
|
|
9d08f5c7c8 | ||
|
|
4fe5f6ca6f | ||
|
|
af505935bd | ||
|
|
4a30f60622 | ||
|
|
24ed04e6c7 | ||
|
|
bed3595314 | ||
|
|
74671263bf | ||
|
|
bdc5e9c7e9 | ||
|
|
8fa1c70a97 | ||
|
|
1343c8fa8d | ||
|
|
5c0f9510a1 | ||
|
|
b2801170fa | ||
|
|
6ac19e8307 | ||
|
|
344241cfbf | ||
|
|
66ebf18fa9 | ||
|
|
e6d6f3317e | ||
|
|
55f66e6bcd | ||
|
|
22e2b0a6a9 | ||
|
|
e24f3828cb | ||
|
|
70f5bdc499 | ||
|
|
8731a39c3c | ||
|
|
e2d9e75475 | ||
|
|
55c7707b94 | ||
|
|
98944d4815 | ||
|
|
faba25acbd | ||
|
|
aab5ee344c | ||
|
|
6880f88dbe | ||
|
|
9177fe86dc | ||
|
|
bf3f8acbc0 | ||
|
|
b4fa9826f2 | ||
|
|
955de3133c | ||
|
|
faac3d0f57 | ||
|
|
3fb1ede770 | ||
|
|
53224886e3 | ||
|
|
e513180d7e | ||
|
|
3116d6df1a | ||
|
|
4284a4d16d | ||
|
|
eadca3b2c0 | ||
|
|
68125af29f | ||
|
|
d436e628fe | ||
|
|
c5483c81de | ||
|
|
9db4a04cd9 | ||
|
|
8943e8c2f6 | ||
|
|
f17fa55426 | ||
|
|
9ff83075d6 | ||
|
|
6b6043afdc | ||
|
|
fd9162e70f | ||
|
|
8331059b7c | ||
|
|
f0992b4914 | ||
|
|
e081a4cfc2 | ||
|
|
895b3ca7c9 | ||
|
|
4d30264950 | ||
|
|
3a352a94f6 | ||
|
|
8623175612 | ||
|
|
ea531777c4 | ||
|
|
2639ef7e13 | ||
|
|
0e76a19c96 | ||
|
|
3ec9eb377d | ||
|
|
44b7000509 | ||
|
|
251307f1e6 | ||
|
|
75e638ec4d | ||
|
|
443ccdd6c0 | ||
|
|
dbf11cc04b | ||
|
|
a5c9729f5f | ||
|
|
a6e2983a22 | ||
|
|
a21f0c46c7 | ||
|
|
ed4a81e852 | ||
|
|
9231218350 | ||
|
|
7d1ea4ee2b | ||
|
|
0c68e51930 | ||
|
|
8e565dc610 | ||
|
|
5b47844fa1 | ||
|
|
0d01d3246a | ||
|
|
bb31c4ba14 | ||
|
|
d9a7952467 | ||
|
|
208dcbbda3 | ||
|
|
ff4494d395 | ||
|
|
db4b32119e | ||
|
|
bfef5b1877 | ||
|
|
91962cc90b | ||
|
|
7a71a5ddd4 | ||
|
|
ca47a589ba | ||
|
|
a33dbd8922 | ||
|
|
aaab4ca741 | ||
|
|
6aadf7429d | ||
|
|
748307bc91 | ||
|
|
559389a319 | ||
|
|
a9140b6a76 | ||
|
|
8f50e873c9 | ||
|
|
9b28b7f78a | ||
|
|
4417097f97 | ||
|
|
93fc036d2c | ||
|
|
7f13fc58b9 | ||
|
|
527d1f6c93 | ||
|
|
0ec3c496d8 | ||
|
|
62ec3de0b1 | ||
|
|
2c47cb6a3b | ||
|
|
dc2b145843 | ||
|
|
c958e1578a | ||
|
|
1e857e5c0a | ||
|
|
9bc8d48415 | ||
|
|
f86c6734a7 | ||
|
|
3d5ff82062 | ||
|
|
8b63483e2e | ||
|
|
c97d4288c0 | ||
|
|
ff75364f22 | ||
|
|
7f1b028099 | ||
|
|
9e70a985e4 | ||
|
|
057cf273dd | ||
|
|
9af621294d | ||
|
|
bfb5a31032 | ||
|
|
6308955e89 | ||
|
|
b0c5b629d7 | ||
|
|
10f965570b | ||
|
|
c24f5200f5 | ||
|
|
f922692a1a | ||
|
|
65c137f67d | ||
|
|
585679969a | ||
|
|
893b67b6c4 | ||
|
|
3c296b440e | ||
|
|
f8a59b52f7 | ||
|
|
17d106cfda | ||
|
|
dcf9d30382 | ||
|
|
eafd7f6b77 | ||
|
|
f31165b442 | ||
|
|
40c5d5871e | ||
|
|
2362754356 | ||
|
|
8ccf3b589e | ||
|
|
2dbc73c75b | ||
|
|
87e1ad7115 | ||
|
|
2e9895aceb | ||
|
|
fbbc573f33 | ||
|
|
8bc8a045b6 | ||
|
|
8c049b29b3 | ||
|
|
f1e783f047 | ||
|
|
3baa6c7145 | ||
|
|
dd28bb46db | ||
|
|
c182091fa9 | ||
|
|
d061572ed6 | ||
|
|
3a943fbc81 | ||
|
|
8a58a51a2c | ||
|
|
e92265ceaa | ||
|
|
b7b1fff1c0 | ||
|
|
886f7021f4 | ||
|
|
2b17188087 | ||
|
|
d498988bd4 | ||
|
|
785a08ba2c | ||
|
|
6aa83102e4 | ||
|
|
97fe41f3ba | ||
|
|
b7cecfbb66 | ||
|
|
7f7d80e653 | ||
|
|
64796a96fc | ||
|
|
83cc7dc86e | ||
|
|
75c9145558 | ||
|
|
869f459d98 | ||
|
|
e00008b663 | ||
|
|
175f2482e1 | ||
|
|
9d91d72a72 | ||
|
|
fd9cd1b98b | ||
|
|
2640040359 | ||
|
|
dec8f8e896 | ||
|
|
b4eff25636 | ||
|
|
2a752e5358 | ||
|
|
b2684837c1 | ||
|
|
2fed2133a5 | ||
|
|
7fc0877c26 | ||
|
|
f7ce774ddc | ||
|
|
174c730f1f | ||
|
|
8ad513e4ec | ||
|
|
a0bf5b4067 | ||
|
|
484f603c9e | ||
|
|
94b16fb327 | ||
|
|
55db7bdf76 | ||
|
|
14af241d5f | ||
|
|
4efa90e42f | ||
|
|
581e1759c4 | ||
|
|
61cdac321e | ||
|
|
59a04137a1 | ||
|
|
eba1a1ffff | ||
|
|
df25b179d7 | ||
|
|
ff2d9ad67d | ||
|
|
485b72cd0d | ||
|
|
57eb5a4328 | ||
|
|
2c4954e31c | ||
|
|
748f2b42e8 | ||
|
|
6bc713bc2e | ||
|
|
b1019d6bab | ||
|
|
268f1872e4 | ||
|
|
60d439cc32 | ||
|
|
36d201ac20 | ||
|
|
d6644bc232 | ||
|
|
7a9b893931 | ||
|
|
235c314e68 | ||
|
|
831253e057 | ||
|
|
d9527902e6 | ||
|
|
290ff79c86 | ||
|
|
8618df09e5 | ||
|
|
6a56c12ef1 | ||
|
|
64ae135f90 | ||
|
|
b18f77c957 | ||
|
|
98da15e801 | ||
|
|
90b0d24c1e | ||
|
|
34d728cae0 | ||
|
|
3eedeb603d | ||
|
|
885bfdade4 | ||
|
|
1382d3bc56 | ||
|
|
9068b221c8 | ||
|
|
b8be61947a | ||
|
|
41811db5aa | ||
|
|
39ba1bcdaf | ||
|
|
d8943274d1 | ||
|
|
67082198aa | ||
|
|
db5dbdea40 | ||
|
|
08eba7077d | ||
|
|
ecf5e7c7b7 | ||
|
|
220023f403 | ||
|
|
0d4e625041 | ||
|
|
3f736e8b80 | ||
|
|
9394c09247 | ||
|
|
e7ab400d29 | ||
|
|
92012dab09 | ||
|
|
26f0a14ac2 | ||
|
|
7a9a2fc76f | ||
|
|
991167fb8b | ||
|
|
451278b48e | ||
|
|
e6f142dc9e | ||
|
|
951709edef | ||
|
|
6cf8083b02 | ||
|
|
3fe32d2a9b | ||
|
|
dc02443a33 | ||
|
|
f785ab553a | ||
|
|
92c015ec83 | ||
|
|
60bfdc0adc | ||
|
|
ae52c83010 | ||
|
|
1c9a549c10 | ||
|
|
4e55b88ec9 | ||
|
|
c87ada5646 | ||
|
|
d2af42431c | ||
|
|
8a96fc7905 | ||
|
|
787edd28c7 | ||
|
|
2e379653a4 | ||
|
|
4541845340 | ||
|
|
59f9cf8aa3 | ||
|
|
7c46d1b348 | ||
|
|
a6330c0fc8 | ||
|
|
f72296c323 | ||
|
|
93c4211da4 | ||
|
|
ea063dd487 | ||
|
|
18815a9a70 | ||
|
|
54271283af | ||
|
|
dab0176938 | ||
|
|
ab6a7b5377 | ||
|
|
1fdd28b974 | ||
|
|
317f59ba63 | ||
|
|
3426c17f16 | ||
|
|
5bc6cbe7f5 | ||
|
|
1eb873bd35 | ||
|
|
5bca02bac2 | ||
|
|
a062b5a355 | ||
|
|
fba2a2e2ca | ||
|
|
6daa69cff9 | ||
|
|
60a3bc5832 | ||
|
|
24ee3401af | ||
|
|
cae1ca8dfb | ||
|
|
35144e6f47 | ||
|
|
e37e07ab3b | ||
|
|
9b9defc0f1 | ||
|
|
feb31fae6e | ||
|
|
04ff5f78a9 | ||
|
|
8922981012 | ||
|
|
d5746ed277 | ||
|
|
dd94653a48 | ||
|
|
d3ed637387 | ||
|
|
6faa9c0b58 | ||
|
|
4c854e00ca | ||
|
|
67bb4cf5ca | ||
|
|
0ed5ce229b | ||
|
|
1065108a42 | ||
|
|
8e7080fc96 | ||
|
|
46d46f9733 | ||
|
|
79e914b0fb | ||
|
|
0075414c8b | ||
|
|
285c122bdc | ||
|
|
be304738e7 | ||
|
|
9f881e9911 | ||
|
|
21cb2e5609 | ||
|
|
bfe0fb91f3 | ||
|
|
37e6f18378 | ||
|
|
15e50fcd34 | ||
|
|
5534153d43 | ||
|
|
f0a9897daf | ||
|
|
a334415ff9 | ||
|
|
75347b7a79 | ||
|
|
b8a756f98d | ||
|
|
3bf23850a3 | ||
|
|
d13eb45565 | ||
|
|
414c616f03 | ||
|
|
bca4df677c | ||
|
|
876584e132 | ||
|
|
d242145c0a | ||
|
|
5640e2eb25 | ||
|
|
db7494b714 | ||
|
|
ada8e19715 | ||
|
|
df7870bfce | ||
|
|
07749a9623 | ||
|
|
8a181f3204 | ||
|
|
fcb0fce799 | ||
|
|
fcb06f82d9 | ||
|
|
75ba35c732 | ||
|
|
1b68cc139f | ||
|
|
084f56d052 | ||
|
|
85caa247f8 | ||
|
|
77904e0c55 | ||
|
|
6f338b6fc7 | ||
|
|
e760122db6 | ||
|
|
8084a225e1 | ||
|
|
9cb01ef8ae | ||
|
|
4cf33cf12a | ||
|
|
64418e0a85 | ||
|
|
f513fdf3c1 | ||
|
|
8290ca0106 | ||
|
|
32fb9cb746 | ||
|
|
807f186f6b | ||
|
|
52202948a9 | ||
|
|
4e545595b7 | ||
|
|
1af1802dbe |
No files matched your search
@@ -106,12 +106,6 @@ LARK_OPEN_APP_ID=""
|
|||||||
LARK_OPEN_APP_SECRET=""
|
LARK_OPEN_APP_SECRET=""
|
||||||
LARK_OPEN_VERIFICATION_TOKEN=""
|
LARK_OPEN_VERIFICATION_TOKEN=""
|
||||||
|
|
||||||
# - WEB3
|
|
||||||
# Used for the Web3 plugin
|
|
||||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
|
|
||||||
ALCHEMY_API_KEY=""
|
|
||||||
INFURA_API_KEY=""
|
|
||||||
|
|
||||||
# - SALESFORCE
|
# - SALESFORCE
|
||||||
# Used for the Salesforce (Sales Cloud) app
|
# Used for the Salesforce (Sales Cloud) app
|
||||||
SALESFORCE_CONSUMER_KEY=""
|
SALESFORCE_CONSUMER_KEY=""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# - SHARED
|
# - SHARED
|
||||||
# - NEXTAUTH
|
# - NEXTAUTH
|
||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
# - ORGANIZATIONS
|
||||||
|
|
||||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||||
@@ -22,6 +23,8 @@ CALCOM_LICENSE_KEY=
|
|||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
|
UPSTASH_REDIS_REST_URL=
|
||||||
|
UPSTASH_REDIS_REST_TOKEN=
|
||||||
|
|
||||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
@@ -32,6 +35,8 @@ PRISMA_GENERATE_DATAPROXY=
|
|||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
|
# Set this to http://app.cal.local:3000 if you want to enable organizations, and
|
||||||
|
# check variable ORGANIZATIONS_ENABLED at the bottom of this file
|
||||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||||
@@ -183,3 +188,26 @@ CSP_POLICY=
|
|||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
|
|
||||||
|
# - ORGANIZATIONS *******************************************************************************************
|
||||||
|
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
||||||
|
# This is mainly needed locally, because for orgs to work a full domain name needs to point
|
||||||
|
# to the app, i.e. app.cal.local instead of using localhost, which is very disruptive
|
||||||
|
#
|
||||||
|
# This variable should only be set to 1 or true if you are in a non-prod environment and you want to
|
||||||
|
# use organizations
|
||||||
|
ORGANIZATIONS_ENABLED=
|
||||||
|
|
||||||
|
# Vercel Config to create subdomains for organizations
|
||||||
|
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
|
||||||
|
PROJECT_ID_VERCEL=
|
||||||
|
# Get it from: https://vercel.com/teams/<TEAM_SLUG>/settings
|
||||||
|
TEAM_ID_VERCEL=
|
||||||
|
# Get it from: https://vercel.com/account/tokens
|
||||||
|
AUTH_BEARER_TOKEN_VERCEL=
|
||||||
|
|
||||||
|
#Enables New booker for Embed only
|
||||||
|
NEW_BOOKER_ENABLED_FOR_EMBED=0
|
||||||
|
|
||||||
|
#Enables New booker for All but Embed requests
|
||||||
|
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||||
@@ -8,8 +8,6 @@ Fixes # (issue)
|
|||||||
Loom Video: https://www.loom.com/
|
Loom Video: https://www.loom.com/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Environment**: Staging(main branch) / Production
|
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete bullets that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
@@ -27,13 +25,16 @@ Fixes # (issue)
|
|||||||
- [ ] Test A
|
- [ ] Test A
|
||||||
- [ ] Test B
|
- [ ] Test B
|
||||||
|
|
||||||
|
## Mandatory Tasks
|
||||||
|
|
||||||
|
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
<!-- Please remove all the irrelevant bullets to your PR -->
|
||||||
|
|
||||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||||
- My code doesn't follow the style guidelines of this project
|
- My code doesn't follow the style guidelines of this project
|
||||||
- I haven't performed a self-review of my own code and corrected any misspellings
|
|
||||||
- I haven't commented my code, particularly in hard-to-understand areas
|
- I haven't commented my code, particularly in hard-to-understand areas
|
||||||
- I haven't checked if my PR needs changes to the documentation
|
- I haven't checked if my PR needs changes to the documentation
|
||||||
- I haven't checked if my changes generate no new warnings
|
- I haven't checked if my changes generate no new warnings
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Cache production build
|
- name: Cache production build
|
||||||
uses: actions/cache@v3
|
uses: buildjet/cache@v3
|
||||||
id: cache-build
|
id: cache-build
|
||||||
env:
|
env:
|
||||||
cache-name: prod-build
|
cache-name: prod-build
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Cache database
|
- name: Cache database
|
||||||
id: cache-db
|
id: cache-db
|
||||||
uses: actions/cache@v3
|
uses: buildjet/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-db
|
cache-name: cache-db
|
||||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Create env file
|
- name: Create env file
|
||||||
uses: actions/cache@v3
|
uses: buildjet/cache@v3
|
||||||
id: env-cache
|
id: env-cache
|
||||||
with:
|
with:
|
||||||
path: gh.env
|
path: gh.env
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node ${{ inputs.node_version }}
|
- name: Use Node ${{ inputs.node_version }}
|
||||||
uses: actions/setup-node@v3
|
uses: buildjet/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node_version }}
|
node-version: ${{ inputs.node_version }}
|
||||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
||||||
@@ -32,7 +32,7 @@ runs:
|
|||||||
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||||
# Yarn cache is also reusable between arch and os.
|
# Yarn cache is also reusable between arch and os.
|
||||||
- name: Restore yarn cache
|
- name: Restore yarn cache
|
||||||
uses: actions/cache@v3
|
uses: buildjet/cache@v3
|
||||||
id: yarn-download-cache
|
id: yarn-download-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||||
@@ -40,10 +40,18 @@ runs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
yarn-download-cache-
|
yarn-download-cache-
|
||||||
|
|
||||||
|
# Invalidated on yarn.lock changes
|
||||||
|
- name: Restore node_modules
|
||||||
|
id: yarn-nm-cache
|
||||||
|
uses: buildjet/cache@v3
|
||||||
|
with:
|
||||||
|
path: "**/node_modules/"
|
||||||
|
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||||
|
|
||||||
# Invalidated on yarn.lock changes
|
# Invalidated on yarn.lock changes
|
||||||
- name: Restore yarn install state
|
- name: Restore yarn install state
|
||||||
id: yarn-install-state-cache
|
id: yarn-install-state-cache
|
||||||
uses: actions/cache@v3
|
uses: buildjet/cache@v3
|
||||||
with:
|
with:
|
||||||
path: .yarn/ci-cache/
|
path: .yarn/ci-cache/
|
||||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||||
@@ -58,5 +66,6 @@ runs:
|
|||||||
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
||||||
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
||||||
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
||||||
|
YARN_NM_MODE: "hardlinks-local" # Reduce node_modules size
|
||||||
# Other environment variables
|
# Other environment variables
|
||||||
HUSKY: "0" # By default do not run HUSKY install
|
HUSKY: "0" # By default do not run HUSKY install
|
||||||
@@ -5,7 +5,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: actions/cache@v2
|
uses: buildjet/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/Library/Caches/ms-playwright
|
~/Library/Caches/ms-playwright
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Add PRs to project Reviewing PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-PR-to-project:
|
||||||
|
name: Add PRs to project Reviewing PRs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v0.1.0
|
||||||
|
with:
|
||||||
|
project-url: https://github.com/orgs/calcom/projects/11
|
||||||
|
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
@@ -1,27 +1,75 @@
|
|||||||
name: "Apply issue labels to PR"
|
name: "Apply issue labels to PR"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened]
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label_on_pr:
|
label_on_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: none
|
||||||
|
issues: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
- name: Apply labels from linked issue to PR
|
- name: Apply labels from linked issue to PR
|
||||||
env:
|
uses: actions/github-script@v5
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
with:
|
||||||
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
script: |
|
||||||
|
async function getLinkedIssues(owner, repo, prNumber) {
|
||||||
|
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
prNumber: prNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await github.graphql(query, variables);
|
||||||
|
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
const linkedIssues = await getLinkedIssues(
|
||||||
|
context.repo.owner,
|
||||||
|
context.repo.repo,
|
||||||
|
pr.number
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelsToAdd = new Set();
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
if (issue.labels && issue.labels.nodes) {
|
||||||
|
for (const label of issue.labels.nodes) {
|
||||||
|
labelsToAdd.add(label.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelsToAdd.size) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
labels: Array.from(labelsToAdd),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
|
ascending: true
|
||||||
days-before-issue-stale: 60
|
days-before-issue-stale: 60
|
||||||
days-before-issue-close: -1
|
days-before-issue-close: -1
|
||||||
days-before-pr-stale: 14
|
days-before-pr-stale: 14
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -25,7 +25,9 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test-e2e:embed-react
|
run: |
|
||||||
|
yarn test-e2e:embed-react
|
||||||
|
yarn workspace @calcom/embed-react packaged:tests
|
||||||
env:
|
env:
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E tests
|
name: E2E tests (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -14,6 +14,10 @@ jobs:
|
|||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
shard: [1, 2, 3, 4, 5]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
@@ -24,12 +28,12 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test-e2e
|
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
env:
|
env:
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
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
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -20,7 +20,7 @@ env:
|
|||||||
INPUT_ENV_PAYMENT_FEE_FIXED: 10
|
INPUT_ENV_PAYMENT_FEE_FIXED: 10
|
||||||
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
INPUT_ENV_SAML_ADMINS: pro@example.com
|
INPUT_ENV_SAML_ADMINS: pro@example.com
|
||||||
INPUT_ENV_NEXTAUTH_URL: http://127.0.0.1:3000/api/auth
|
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000
|
||||||
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
|
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
|
||||||
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
|
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
|
||||||
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
@@ -31,19 +31,21 @@ env:
|
|||||||
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
|
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
|
||||||
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
|
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
|
||||||
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
|
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
|
||||||
|
INPUT_ENV_SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||||
|
INPUT_ENV_SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_env_file:
|
create_env_file:
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ozaytsev86/create-env-file@v1
|
- uses: ozaytsev86/create-env-file@v1
|
||||||
with:
|
with:
|
||||||
file-name: ${{ github.workspace }}/gh.env
|
file-name: ${{ github.workspace }}/gh.env
|
||||||
- uses: actions/cache@v3
|
- uses: buildjet/cache@v3
|
||||||
id: env-cache
|
id: env-cache
|
||||||
with:
|
with:
|
||||||
path: gh.env
|
path: gh.env
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- name: Save Code Linting Reports
|
- name: Run Linting with Reports
|
||||||
run: yarn lint:report
|
run: yarn lint:report
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
analyze:
|
analyze:
|
||||||
needs: build
|
needs: build
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
runs-on: ubuntu-latest
|
name: Detect changes
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
@@ -33,12 +34,15 @@ jobs:
|
|||||||
app-store:
|
app-store:
|
||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
- 'packages/app-store/**'
|
- 'packages/app-store/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
embed:
|
embed:
|
||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
- 'packages/embeds/**'
|
- 'packages/embeds/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
embed-react:
|
embed-react:
|
||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
- 'packages/embeds/**'
|
- 'packages/embeds/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
name: Create env file
|
name: Create env file
|
||||||
@@ -66,6 +70,12 @@ jobs:
|
|||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-without-database:
|
||||||
|
name: Production build (without database)
|
||||||
|
needs: env
|
||||||
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: E2E tests
|
name: E2E tests
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
@@ -101,7 +111,7 @@ jobs:
|
|||||||
required:
|
required:
|
||||||
needs: [lint, type-check, test, build, e2e]
|
needs: [lint, type-check, test, build, e2e]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Production Build (without database)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
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-build
|
||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
const https = require("https");
|
|
||||||
|
|
||||||
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|
||||||
// Get the labels from issues linked to the PR
|
|
||||||
const query = `
|
|
||||||
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
pullRequest(number: $prNumber) {
|
|
||||||
closingIssuesReferences(first: 10) {
|
|
||||||
nodes {
|
|
||||||
number
|
|
||||||
labels(first: 10) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const graphqlData = JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables: {
|
|
||||||
owner: pr.base.repo.owner.login,
|
|
||||||
repo: pr.base.repo.name,
|
|
||||||
prNumber: pr.number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
hostname: "api.github.com",
|
|
||||||
path: "/graphql",
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Content-Length": graphqlData.length,
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
"User-Agent": "Node.js",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the native Node.js request library to make the request
|
|
||||||
let linkedIssues;
|
|
||||||
linkedIssues = await new Promise((resolve, reject) => {
|
|
||||||
const request = https.request(requestOptions, (response) => {
|
|
||||||
let responseBody = "";
|
|
||||||
response.on("data", (chunk) => {
|
|
||||||
responseBody += chunk;
|
|
||||||
});
|
|
||||||
response.on("end", () => {
|
|
||||||
resolve(JSON.parse(responseBody)?.data?.repository?.pullRequest?.closingIssuesReferences?.nodes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.write(graphqlData);
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!linkedIssues || linkedIssues.length === 0) {
|
|
||||||
console.log("No issue linked.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over linked issues and apply labels to PR
|
|
||||||
for (const issue of linkedIssues) {
|
|
||||||
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
|
||||||
|
|
||||||
if (!labels || labels.length === 0) {
|
|
||||||
console.log(`No labels found on the linked issue #${issue.number}.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelsData = JSON.stringify({
|
|
||||||
labels: labels,
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
hostname: "api.github.com",
|
|
||||||
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Content-Length": labelsData.length,
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
"User-Agent": "Node.js",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let labelResult;
|
|
||||||
labelResult = await new Promise((resolve, reject) => {
|
|
||||||
const request = https.request(requestOptions, (response) => {
|
|
||||||
let responseBody = "";
|
|
||||||
response.on("data", (chunk) => {
|
|
||||||
responseBody += chunk;
|
|
||||||
});
|
|
||||||
response.on("data", () => {});
|
|
||||||
response.on("end", () => {
|
|
||||||
resolve(JSON.parse(responseBody));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.write(labelsData);
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (labelResult.message) {
|
|
||||||
console.log(`Error labelling PR: ${labelResult.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Applied labels: ${labels.join(", ")} to PR #${pr.number} from linked issue #${issue.number}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the PR data and GitHub token to the script
|
|
||||||
(async () => {
|
|
||||||
if (!process.env.PR_DATA) {
|
|
||||||
console.log("No PR data found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prData = JSON.parse(process.env.PR_DATA);
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
await applyLabelFromLinkedIssueToPR(prData, token);
|
|
||||||
})();
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: "Validate PRs"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
id: lint_pr_title
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
# When the previous steps fails, the workflow would stop. By adding this
|
||||||
|
# condition you can continue the execution with the populated error message.
|
||||||
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Hey there and thank you for opening this pull request! 👋🏼
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Delete a previous comment when the issue has been resolved
|
||||||
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Thank you for following the naming conventions! 🙏
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: "Welcome new contributors"
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: opened
|
||||||
|
pull_request:
|
||||||
|
types: opened
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
welcome-message:
|
||||||
|
name: Welcoming New Users
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
needs: semantics
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
steps:
|
||||||
|
- uses: actions/first-interaction@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pr-message: |-
|
||||||
|
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
|
||||||
|
Feel free to join the conversation at [slack](https://calendso.slack.com/)
|
||||||
|
issue-message: |
|
||||||
|
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
|
||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
name: Yarn install & cache
|
name: Yarn install & cache
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
Vendored
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
"ban.spellright", // Spell check for docs
|
"ban.spellright", // Spell check for docs
|
||||||
"stripe.vscode-stripe", // stripe VSCode extension
|
"stripe.vscode-stripe", // stripe VSCode extension
|
||||||
"Prisma.prisma", // syntax|format|completion for prisma
|
"Prisma.prisma", // syntax|format|completion for prisma
|
||||||
"rebornix.project-snippets" // Share useful snippets between collaborators
|
"rebornix.project-snippets", // Share useful snippets between collaborators
|
||||||
|
"inlang.vs-code-extension" // improved i18n DX
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
|||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee and https://github.com/calcom/cal.com/tree/main/apps/api/ directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ To get a local copy up and running, please follow these simple steps.
|
|||||||
|
|
||||||
Here is what you need to be able to run Cal.com.
|
Here is what you need to be able to run Cal.com.
|
||||||
|
|
||||||
- Node.js (Version: >=15.x <17)
|
- Node.js (Version: >=18.x)
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
- Yarn _(recommended)_
|
- Yarn _(recommended)_
|
||||||
|
|
||||||
@@ -121,9 +121,9 @@ Here is what you need to be able to run Cal.com.
|
|||||||
```sh
|
```sh
|
||||||
git clone https://github.com/calcom/cal.com.git
|
git clone https://github.com/calcom/cal.com.git
|
||||||
```
|
```
|
||||||
> If you are on windows, run the following command on `gitbash` with admin privileges: <br>
|
|
||||||
```git clone -c core.symlinks=true https://github.com/calcom/cal.com.git``` <br>
|
> If you are on windows, run the following command on `gitbash` with admin privileges: <br> > `git clone -c core.symlinks=true https://github.com/calcom/cal.com.git` <br>
|
||||||
See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
> See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
||||||
|
|
||||||
1. Go to the project folder
|
1. Go to the project folder
|
||||||
|
|
||||||
@@ -158,6 +158,7 @@ yarn dx
|
|||||||
```sh
|
```sh
|
||||||
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Gitpod Setup
|
#### Gitpod Setup
|
||||||
|
|
||||||
1. Click the button below to open this project in Gitpod.
|
1. Click the button below to open this project in Gitpod.
|
||||||
@@ -166,8 +167,6 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Manual setup
|
#### Manual setup
|
||||||
|
|
||||||
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, and `<db-port>` with their applicable values
|
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, and `<db-port>` with their applicable values
|
||||||
@@ -175,6 +174,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
```
|
```
|
||||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
|
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
|
||||||
|
|
||||||
@@ -188,11 +188,12 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||

|

|
||||||
|
|
||||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||||
|
|
||||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||||
|
|
||||||
@@ -201,9 +202,18 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
|
In a development environment, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn workspace @calcom/prisma db-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
In a production environment, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn workspace @calcom/prisma db-deploy
|
yarn workspace @calcom/prisma db-deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -311,7 +321,7 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
|
|||||||
### Heroku
|
### Heroku
|
||||||
|
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
||||||
<img width="185px" height="auto" src="https://herokucdn.com/deploy/button.svg" alt="Deploy">
|
<img width="185px" height="auto" src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### Railway
|
### Railway
|
||||||
@@ -416,7 +426,7 @@ Make sure to complete section "Obtaining the Google API Credentials". After the
|
|||||||
following
|
following
|
||||||
|
|
||||||
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
||||||
1. Under 'OAuth concent screen', click "PUBLISH APP"
|
1. Under 'OAuth consent screen', click "PUBLISH APP"
|
||||||
|
|
||||||
### Obtaining Microsoft Graph Client ID and Secret
|
### Obtaining Microsoft Graph Client ID and Secret
|
||||||
|
|
||||||
@@ -482,7 +492,9 @@ following
|
|||||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||||
|
|
||||||
### Obtaining Zoho Bigin Client ID and Secret
|
### Obtaining Zoho Bigin Client ID and Secret
|
||||||
|
|
||||||
[Follow these steps](./packages/app-store/zoho-bigin/)
|
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require("@calcom/config/eslint-preset");
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
The Cal.com Commercial License (the “Commercial License”)
|
||||||
|
Copyright (c) 2020-present Cal.com, Inc
|
||||||
|
|
||||||
|
With regard to the Cal.com Software:
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, the Cal.com Subscription Terms available
|
||||||
|
at https://cal.com/terms, or other agreements governing
|
||||||
|
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||||
|
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
|
||||||
|
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
|
||||||
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
|
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
and to all such modifications and/or patches, and all such modifications and/or
|
||||||
|
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||||
|
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||||
|
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||||
|
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
|
||||||
|
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||||
|
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||||
|
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell the Software.
|
||||||
|
|
||||||
|
This Commercial License applies only to the part of this Software that is not distributed under
|
||||||
|
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
|
||||||
|
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
|
||||||
|
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
|
||||||
|
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
|
||||||
|
be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Cal.com Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
|
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Cal.com Public API
|
# Commercial Cal.com Public API
|
||||||
|
|
||||||
Welcome to the Public API ("/apps/api") of the Cal.com.
|
Welcome to the Public API ("/apps/api") of the Cal.com.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
afterEach((done) => {
|
|
||||||
prisma.$disconnect().then();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { _BookingModel as Booking, _AttendeeModel, _UserModel } from "@calcom/prisma/zod";
|
import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod";
|
||||||
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
|
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||||
@@ -49,6 +49,15 @@ export const schemaBookingReadPublic = Booking.extend({
|
|||||||
locale: true,
|
locale: true,
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
payment: z
|
||||||
|
.array(
|
||||||
|
_PaymentModel.pick({
|
||||||
|
id: true,
|
||||||
|
success: true,
|
||||||
|
paymentOption: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}).pick({
|
}).pick({
|
||||||
id: true,
|
id: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
@@ -61,6 +70,7 @@ export const schemaBookingReadPublic = Booking.extend({
|
|||||||
timeZone: true,
|
timeZone: true,
|
||||||
attendees: true,
|
attendees: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
payment: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
status: true,
|
status: true,
|
||||||
responses: true,
|
responses: true,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { withValidation } from "next-validations";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { baseApiParams } from "./baseApiParams";
|
||||||
|
|
||||||
|
// Extracted out as utility function so can be reused
|
||||||
|
// at different endpoints that require this validation.
|
||||||
|
export const schemaQueryAttendeeEmail = baseApiParams.extend({
|
||||||
|
attendeeEmail: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
|
||||||
|
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const withValidQueryAttendeeEmail = withValidation({
|
||||||
|
schema: schemaQueryAttendeeEmail,
|
||||||
|
type: "Zod",
|
||||||
|
mode: "query",
|
||||||
|
});
|
||||||
@@ -11,17 +11,13 @@
|
|||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=3002 next dev",
|
"dev": "PORT=3002 next dev",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/api.json",
|
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3002 next start",
|
"start": "PORT=3002 next start",
|
||||||
"test": "jest --detectOpenHandles --passWithNoTests",
|
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/tsconfig": "*",
|
||||||
"@calcom/types": "*",
|
"@calcom/types": "*",
|
||||||
"babel-jest": "^28.1.0",
|
|
||||||
"jest": "^28.1.0",
|
|
||||||
"node-mocks-http": "^1.11.0"
|
"node-mocks-http": "^1.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -36,9 +32,9 @@
|
|||||||
"@sentry/nextjs": "^7.20.0",
|
"@sentry/nextjs": "^7.20.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "~13.2.1",
|
"next": "^13.4.6",
|
||||||
"next-api-middleware": "^1.0.1",
|
"next-api-middleware": "^1.0.1",
|
||||||
"next-axiom": "^0.16.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
"next-validations": "^0.2.0",
|
"next-validations": "^0.2.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import type { NextApiRequest } from "next";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import { availabilityUserSelect } from "@calcom/prisma";
|
import { availabilityUserSelect } from "@calcom/prisma";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +99,9 @@ import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
|||||||
* 404:
|
* 404:
|
||||||
* description: User not found | Team not found | Team has no members
|
* description: User not found | Team not found | Team has no members
|
||||||
*/
|
*/
|
||||||
|
interface MemberRoles {
|
||||||
|
[userId: number | string]: MembershipRole;
|
||||||
|
}
|
||||||
|
|
||||||
const availabilitySchema = z
|
const availabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -113,10 +118,12 @@ const availabilitySchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { prisma, isAdmin } = req;
|
const { prisma, isAdmin, userId: reqUserId } = req;
|
||||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
if (!teamId)
|
if (!teamId)
|
||||||
return getUserAvailability({
|
return getUserAvailability({
|
||||||
|
orgSlug: isValidOrgDomain ? currentOrgDomain || undefined : undefined,
|
||||||
username,
|
username,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
@@ -129,12 +136,26 @@ async function handler(req: NextApiRequest) {
|
|||||||
});
|
});
|
||||||
if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" });
|
if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" });
|
||||||
if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" });
|
if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" });
|
||||||
const allMemberIds = team.members.map((membership) => membership.userId);
|
const allMemberIds = team.members.reduce((allMemberIds: number[], member) => {
|
||||||
|
if (member.accepted) {
|
||||||
|
allMemberIds.push(member.userId);
|
||||||
|
}
|
||||||
|
return allMemberIds;
|
||||||
|
}, []);
|
||||||
const members = await prisma.user.findMany({
|
const members = await prisma.user.findMany({
|
||||||
where: { id: { in: allMemberIds } },
|
where: { id: { in: allMemberIds } },
|
||||||
select: availabilityUserSelect,
|
select: availabilityUserSelect,
|
||||||
});
|
});
|
||||||
if (!isAdmin) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
const memberRoles: MemberRoles = team.members.reduce((acc: MemberRoles, membership) => {
|
||||||
|
acc[membership.userId] = membership.role;
|
||||||
|
return acc;
|
||||||
|
}, {} as MemberRoles);
|
||||||
|
// check if the user is a team Admin or Owner, if it is a team request, or a system Admin
|
||||||
|
const isUserAdminOrOwner =
|
||||||
|
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||||
|
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||||
|
isAdmin;
|
||||||
|
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
const availabilities = members.map(async (user) => {
|
const availabilities = members.map(async (user) => {
|
||||||
return {
|
return {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -32,40 +32,65 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: "#/components/schemas/ArrayOfBookings"
|
* $ref: "#/components/schemas/Booking"
|
||||||
* examples:
|
* examples:
|
||||||
* bookings:
|
* booking:
|
||||||
* value: [
|
* value:
|
||||||
* {
|
* {
|
||||||
* "id": 1,
|
* "booking": {
|
||||||
* "description": "Meeting with John",
|
* "id": 91,
|
||||||
* "eventTypeId": 2,
|
* "userId": 5,
|
||||||
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
|
* "description": "",
|
||||||
* "title": "Business Meeting",
|
* "eventTypeId": 7,
|
||||||
* "startTime": "2023-04-20T10:00:00.000Z",
|
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||||
* "endTime": "2023-04-20T11:00:00.000Z",
|
* "title": "60min between Pro Example and John Doe",
|
||||||
* "timeZone": "Europe/London",
|
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||||
* "attendees": [
|
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||||
* {
|
* "attendees": [
|
||||||
* "email": "example@cal.com",
|
* {
|
||||||
* "name": "John Doe",
|
* "email": "john.doe@example.com",
|
||||||
* "timeZone": "Europe/London",
|
* "name": "John Doe",
|
||||||
|
* "timeZone": "Asia/Kolkata",
|
||||||
|
* "locale": "en"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "user": {
|
||||||
|
* "email": "pro@example.com",
|
||||||
|
* "name": "Pro Example",
|
||||||
|
* "timeZone": "Asia/Kolkata",
|
||||||
* "locale": "en"
|
* "locale": "en"
|
||||||
|
* },
|
||||||
|
* "payment": [
|
||||||
|
* {
|
||||||
|
* "id": 1,
|
||||||
|
* "success": true,
|
||||||
|
* "paymentOption": "ON_BOOKING"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "metadata": {},
|
||||||
|
* "status": "ACCEPTED",
|
||||||
|
* "responses": {
|
||||||
|
* "email": "john.doe@example.com",
|
||||||
|
* "name": "John Doe",
|
||||||
|
* "location": {
|
||||||
|
* "optionValue": "",
|
||||||
|
* "value": "inPerson"
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
|
||||||
* 401:
|
* 401:
|
||||||
* description: Authorization information is missing or invalid.
|
* description: Authorization information is missing or invalid.
|
||||||
* 404:
|
* 404:
|
||||||
* description: Booking was not found
|
* description: Booking was not found
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function getHandler(req: NextApiRequest) {
|
export async function getHandler(req: NextApiRequest) {
|
||||||
const { prisma, query } = req;
|
const { prisma, query } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const booking = await prisma.booking.findUnique({
|
const booking = await prisma.booking.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { attendees: true, user: true },
|
include: { attendees: true, user: true, payment: true },
|
||||||
});
|
});
|
||||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* status:
|
* status:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* description: 'Description of the meeting'
|
||||||
* examples:
|
* examples:
|
||||||
* editBooking:
|
* editBooking:
|
||||||
* value:
|
* value:
|
||||||
@@ -79,7 +82,15 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
* "startTime": "2023-05-24T13:00:00.000Z",
|
||||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
* "endTime": "2023-05-24T13:30:00.000Z",
|
||||||
* "metadata": {},
|
* "metadata": {},
|
||||||
* "status": "CANCELLED"
|
* "status": "CANCELLED",
|
||||||
|
* "responses": {
|
||||||
|
* "email": "john.doe@example.com",
|
||||||
|
* "name": "John Doe",
|
||||||
|
* "location": {
|
||||||
|
* "optionValue": "",
|
||||||
|
* "value": "inPerson"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* 400:
|
* 400:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
|||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
import { schemaBookingReadPublic } from "~/lib/validations/booking";
|
import { schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||||
|
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +32,19 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
|||||||
* items:
|
* items:
|
||||||
* type: integer
|
* type: integer
|
||||||
* example: [2, 3, 4]
|
* example: [2, 3, 4]
|
||||||
|
* - in: query
|
||||||
|
* name: attendeeEmail
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* oneOf:
|
||||||
|
* - type: string
|
||||||
|
* format: email
|
||||||
|
* example: john.doe@example.com
|
||||||
|
* - type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* example: [john.doe@example.com, jane.doe@example.com]
|
||||||
* operationId: listBookings
|
* operationId: listBookings
|
||||||
* tags:
|
* tags:
|
||||||
* - bookings
|
* - bookings
|
||||||
@@ -45,22 +59,47 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
|||||||
* bookings:
|
* bookings:
|
||||||
* value: [
|
* value: [
|
||||||
* {
|
* {
|
||||||
* "id": 1,
|
* "booking": {
|
||||||
* "description": "Meeting with John",
|
* "id": 91,
|
||||||
* "eventTypeId": 2,
|
* "userId": 5,
|
||||||
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
|
* "description": "",
|
||||||
* "title": "Business Meeting",
|
* "eventTypeId": 7,
|
||||||
* "startTime": "2023-04-20T10:00:00.000Z",
|
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||||
* "endTime": "2023-04-20T11:00:00.000Z",
|
* "title": "60min between Pro Example and John Doe",
|
||||||
* "timeZone": "Europe/London",
|
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||||
* "attendees": [
|
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||||
* {
|
* "attendees": [
|
||||||
* "email": "example@cal.com",
|
* {
|
||||||
* "name": "John Doe",
|
* "email": "john.doe@example.com",
|
||||||
* "timeZone": "Europe/London",
|
* "name": "John Doe",
|
||||||
|
* "timeZone": "Asia/Kolkata",
|
||||||
|
* "locale": "en"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "user": {
|
||||||
|
* "email": "pro@example.com",
|
||||||
|
* "name": "Pro Example",
|
||||||
|
* "timeZone": "Asia/Kolkata",
|
||||||
* "locale": "en"
|
* "locale": "en"
|
||||||
|
* },
|
||||||
|
* "payment": [
|
||||||
|
* {
|
||||||
|
* "id": 1,
|
||||||
|
* "success": true,
|
||||||
|
* "paymentOption": "ON_BOOKING"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "metadata": {},
|
||||||
|
* "status": "ACCEPTED",
|
||||||
|
* "responses": {
|
||||||
|
* "email": "john.doe@example.com",
|
||||||
|
* "name": "John Doe",
|
||||||
|
* "location": {
|
||||||
|
* "optionValue": "",
|
||||||
|
* "value": "inPerson"
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
* }
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
* 401:
|
* 401:
|
||||||
@@ -69,26 +108,42 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
|||||||
* description: No bookings were found
|
* description: No bookings were found
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
/**
|
||||||
const { userId, isAdmin, prisma } = req;
|
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||||
const args: Prisma.BookingFindManyArgs = {};
|
*
|
||||||
args.include = {
|
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
||||||
attendees: true,
|
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
||||||
user: true,
|
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
||||||
};
|
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
||||||
|
*
|
||||||
/** Only admins can query other users */
|
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
||||||
if (isAdmin && req.query.userId) {
|
*/
|
||||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
function buildWhereClause(
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
userId: number,
|
||||||
const users = await prisma.user.findMany({
|
attendeeEmails: string[],
|
||||||
where: { id: { in: userIds } },
|
userIds: number[] = [],
|
||||||
select: { email: true },
|
userEmails: string[] = []
|
||||||
});
|
) {
|
||||||
const userEmails = users.map((u) => u.email);
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
args.where = {
|
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : { userId };
|
||||||
|
let whereClause = {};
|
||||||
|
if (filterByAttendeeEmails) {
|
||||||
|
whereClause = {
|
||||||
|
AND: [
|
||||||
|
userFilter,
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: attendeeEmails },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
whereClause = {
|
||||||
OR: [
|
OR: [
|
||||||
{ userId: { in: userIds } },
|
userFilter,
|
||||||
{
|
{
|
||||||
attendees: {
|
attendees: {
|
||||||
some: {
|
some: {
|
||||||
@@ -98,7 +153,45 @@ async function handler(req: NextApiRequest) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} else if (!isAdmin) {
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...whereClause,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest) {
|
||||||
|
const { userId, isAdmin, prisma } = req;
|
||||||
|
const args: Prisma.BookingFindManyArgs = {};
|
||||||
|
args.include = {
|
||||||
|
attendees: true,
|
||||||
|
user: true,
|
||||||
|
payment: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
|
||||||
|
const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail)
|
||||||
|
? queryFilterForAttendeeEmails.attendeeEmail
|
||||||
|
: typeof queryFilterForAttendeeEmails.attendeeEmail === "string"
|
||||||
|
? [queryFilterForAttendeeEmails.attendeeEmail]
|
||||||
|
: [];
|
||||||
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
|
||||||
|
/** Only admins can query other users */
|
||||||
|
if (isAdmin) {
|
||||||
|
if (req.query.userId) {
|
||||||
|
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||||
|
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: { id: { in: userIds } },
|
||||||
|
select: { email: true },
|
||||||
|
});
|
||||||
|
const userEmails = users.map((u) => u.email);
|
||||||
|
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
|
||||||
|
} else if (filterByAttendeeEmails) {
|
||||||
|
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: {
|
select: {
|
||||||
@@ -108,20 +201,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new HttpError({ message: "User not found", statusCode: 500 });
|
throw new HttpError({ message: "User not found", statusCode: 500 });
|
||||||
}
|
}
|
||||||
args.where = {
|
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attendees: {
|
|
||||||
some: {
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const data = await prisma.booking.findMany(args);
|
const data = await prisma.booking.findMany(args);
|
||||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||||
|
|||||||
@@ -26,14 +26,10 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* required:
|
* required:
|
||||||
* - eventTypeId
|
* - eventTypeId
|
||||||
* - start
|
* - start
|
||||||
* - end
|
* - responses
|
||||||
* - name
|
|
||||||
* - email
|
|
||||||
* - timeZone
|
* - timeZone
|
||||||
* - language
|
* - language
|
||||||
* - metadata
|
* - metadata
|
||||||
* - customInputs
|
|
||||||
* - location
|
|
||||||
* properties:
|
* properties:
|
||||||
* eventTypeId:
|
* eventTypeId:
|
||||||
* type: integer
|
* type: integer
|
||||||
@@ -46,29 +42,33 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* type: string
|
* type: string
|
||||||
* format: date-time
|
* format: date-time
|
||||||
* description: 'End time of the Event'
|
* description: 'End time of the Event'
|
||||||
* name:
|
* responses:
|
||||||
* type: string
|
* type: object
|
||||||
* description: 'Name of the Attendee'
|
* required:
|
||||||
* email:
|
* - name
|
||||||
* type: string
|
* - email
|
||||||
* format: email
|
* - location
|
||||||
* description: 'Email ID of the Attendee'
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description: 'Attendee full name'
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: 'Attendee email address'
|
||||||
|
* location:
|
||||||
|
* type: string
|
||||||
|
* description: 'Meeting location'
|
||||||
|
* metadata:
|
||||||
|
* type: object
|
||||||
|
* properties: {}
|
||||||
|
* description: 'Any metadata associated with the booking'
|
||||||
* timeZone:
|
* timeZone:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 'TimeZone of the Attendee'
|
* description: 'TimeZone of the Attendee'
|
||||||
* language:
|
* language:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 'Language of the Attendee'
|
* description: 'Language of the Attendee'
|
||||||
* metadata:
|
|
||||||
* type: object
|
|
||||||
* properties: {}
|
|
||||||
* description: 'Any metadata associated with the booking'
|
|
||||||
* customInputs:
|
|
||||||
* type: array
|
|
||||||
* items: {}
|
|
||||||
* location:
|
|
||||||
* type: string
|
|
||||||
* description: 'Meeting location'
|
|
||||||
* title:
|
* title:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 'Booking event title'
|
* description: 'Booking event title'
|
||||||
@@ -97,13 +97,14 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* "eventTypeId": 2323232,
|
* "eventTypeId": 2323232,
|
||||||
* "start": "2023-05-24T13:00:00.000Z",
|
* "start": "2023-05-24T13:00:00.000Z",
|
||||||
* "end": "2023-05-24T13:30:00.000Z",
|
* "end": "2023-05-24T13:30:00.000Z",
|
||||||
* "name": "Hello Hello",
|
* "responses":{
|
||||||
* "email": "hello@gmail.com",
|
* "name": "Hello Hello",
|
||||||
|
* "email": "hello@gmail.com",
|
||||||
|
* "metadata": {},
|
||||||
|
* "location": "Calcom HQ",
|
||||||
|
* },
|
||||||
* "timeZone": "Europe/London",
|
* "timeZone": "Europe/London",
|
||||||
* "language": "en",
|
* "language": "en",
|
||||||
* "metadata": {},
|
|
||||||
* "customInputs": [],
|
|
||||||
* "location": "Calcom HQ",
|
|
||||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||||
* "description": null,
|
* "description": null,
|
||||||
* "status": "PENDING",
|
* "status": "PENDING",
|
||||||
@@ -118,53 +119,50 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* examples:
|
* examples:
|
||||||
* bookings:
|
* booking created successfully example:
|
||||||
* value:
|
* value:
|
||||||
* {
|
* {
|
||||||
* "id": 11223344,
|
* "booking": {
|
||||||
* "uid": "5yUjmAYTDF6MXo98re8SkX",
|
* "id": 91,
|
||||||
* "userId": 123,
|
* "userId": 5,
|
||||||
* "eventTypeId": 2323232,
|
* "description": "",
|
||||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
* "eventTypeId": 7,
|
||||||
* "description": null,
|
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||||
* "customInputs": {},
|
* "title": "60min between Pro Example and John Doe",
|
||||||
* "responses": null,
|
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
* "attendees": [
|
||||||
* "location": "Calcom HQ",
|
* {
|
||||||
* "createdAt": "2023-04-19T10:17:58.580Z",
|
* "email": "john.doe@example.com",
|
||||||
* "updatedAt": null,
|
* "name": "John Doe",
|
||||||
* "status": "PENDING",
|
* "timeZone": "Asia/Kolkata",
|
||||||
* "paid": false,
|
* "locale": "en"
|
||||||
* "destinationCalendarId": 2180,
|
* }
|
||||||
* "cancellationReason": null,
|
* ],
|
||||||
* "rejectionReason": null,
|
* "user": {
|
||||||
* "dynamicEventSlugRef": null,
|
* "email": "pro@example.com",
|
||||||
* "dynamicGroupSlugRef": null,
|
* "name": "Pro Example",
|
||||||
* "rescheduled": null,
|
* "timeZone": "Asia/Kolkata",
|
||||||
* "fromReschedule": null,
|
* "locale": "en"
|
||||||
* "recurringEventId": null,
|
* },
|
||||||
* "smsReminderNumber": null,
|
* "payment": [
|
||||||
* "scheduledJobs": [],
|
* {
|
||||||
* "metadata": {},
|
* "id": 1,
|
||||||
* "isRecorded": false,
|
* "success": true,
|
||||||
* "user": {
|
* "paymentOption": "ON_BOOKING"
|
||||||
* "email": "test@cal.com",
|
* }
|
||||||
* "name": "Syed Ali Shahbaz",
|
* ],
|
||||||
* "timeZone": "Asia/Calcutta"
|
* "metadata": {},
|
||||||
* },
|
* "status": "ACCEPTED",
|
||||||
* "attendees": [
|
* "responses": {
|
||||||
* {
|
* "email": "john.doe@example.com",
|
||||||
* "id": 12345,
|
* "name": "John Doe",
|
||||||
* "email": "hello@gmail.com",
|
* "location": {
|
||||||
* "name": "Hello Hello",
|
* "optionValue": "",
|
||||||
* "timeZone": "Europe/London",
|
* "value": "inPerson"
|
||||||
* "locale": "en",
|
* }
|
||||||
* "bookingId": 11223344
|
|
||||||
* }
|
* }
|
||||||
* ],
|
* }
|
||||||
* "payment": [],
|
|
||||||
* "references": []
|
|
||||||
* }
|
* }
|
||||||
* 400:
|
* 400:
|
||||||
* description: |
|
* description: |
|
||||||
|
|||||||
@@ -98,6 +98,25 @@ const swaggerHandler = withSwagger({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
payment: {
|
||||||
|
type: Array,
|
||||||
|
items: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "number",
|
||||||
|
example: 1,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
example: true,
|
||||||
|
},
|
||||||
|
paymentOption: {
|
||||||
|
type: "string",
|
||||||
|
example: "ON_BOOKING",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
|
|
||||||
export default function getCalLink(eventType: {
|
export default function getCalLink(eventType: {
|
||||||
team?: { slug: string | null } | null;
|
team?: { slug: string | null } | null;
|
||||||
owner: { username: string | null } | null;
|
owner?: { username: string | null } | null;
|
||||||
users?: { username: string | null }[];
|
users?: { username: string | null }[];
|
||||||
slug: string;
|
slug: string;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
|
const ctx = await createContext({ req, res });
|
||||||
|
const caller = viewerRouter.createCaller(ctx);
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return await caller.slots.getSchedule(req.query as any /* Let tRPC handle this */);
|
||||||
|
} catch (cause) {
|
||||||
|
if (cause instanceof TRPCError) {
|
||||||
|
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||||
|
throw new HttpError({ statusCode, message: cause.message });
|
||||||
|
}
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(handler);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
|
||||||
|
export default withMiddleware()(
|
||||||
|
defaultHandler({
|
||||||
|
GET: import("./_get"),
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { prismaMock } from "../../../../../tests/config/singleton";
|
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
import handler from "../../../pages/api/bookings/_post";
|
import handler from "../../../pages/api/bookings/_post";
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
jest.mock("@calcom/features/webhooks/lib/sendPayload");
|
vi.mock("@calcom/features/webhooks/lib/sendPayload");
|
||||||
jest.mock("@calcom/lib/server/i18n", () => {
|
vi.mock("@calcom/lib/server/i18n", () => {
|
||||||
return {
|
return {
|
||||||
getTranslation: (key: string) => key,
|
getTranslation: (key: string) => key,
|
||||||
};
|
};
|
||||||
@@ -151,7 +152,7 @@ describe("POST /api/bookings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xdescribe("Success", () => {
|
describe("Success", () => {
|
||||||
describe("Regular event-type", () => {
|
describe("Regular event-type", () => {
|
||||||
test("Creates one single booking", async () => {
|
test("Creates one single booking", async () => {
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ export function VariantsTable({
|
|||||||
<table>
|
<table>
|
||||||
<RowTitles titles={titles} />
|
<RowTitles titles={titles} />
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<tr className="p-2 pr-6 pb-6" key={column.props.variant}>
|
<tr className="p-2 pb-6 pr-6" key={column.props.variant}>
|
||||||
<th
|
<th
|
||||||
className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]"
|
className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]"
|
||||||
key={column.props.variant}>
|
key={column.props.variant}>
|
||||||
{column.props.variant}
|
{column.props.variant}
|
||||||
</th>
|
</th>
|
||||||
{React.Children.count(column.props.children) &&
|
{React.Children.count(column.props.children) &&
|
||||||
React.Children.map(column.props.children, (cell) => (
|
React.Children.map(column.props.children, (cell) => (
|
||||||
<td className="p-2 pr-6 pb-6" style={{ minWidth: `${columnMinWidth}px` }}>
|
<td className="p-2 pb-6 pr-6" style={{ minWidth: `${columnMinWidth}px` }}>
|
||||||
{cell}
|
{cell}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
@@ -70,9 +70,9 @@ export function VariantRow({ children }: RowProps) {
|
|||||||
export function RowTitles({ titles }: { titles: string[] }) {
|
export function RowTitles({ titles }: { titles: string[] }) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" />
|
<th className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]" />
|
||||||
{titles.map((title) => (
|
{titles.map((title) => (
|
||||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" key={title}>
|
<th className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]" key={title}>
|
||||||
{title}
|
{title}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@
|
|||||||
"@radix-ui/react-avatar": "^1.0.0",
|
"@radix-ui/react-avatar": "^1.0.0",
|
||||||
"@radix-ui/react-collapsible": "^1.0.0",
|
"@radix-ui/react-collapsible": "^1.0.0",
|
||||||
"@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": "^2.0.5",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.2",
|
"@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.2.1",
|
"next": "^13.4.6",
|
||||||
"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"
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"@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.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
"storybook-addon-designs": "^6.3.1",
|
"storybook-addon-designs": "^6.3.1",
|
||||||
"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.3.1",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^2.9.15"
|
"vite": "^4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +209,7 @@
|
|||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #E2FBE8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #FCEED8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #F9E3E2;
|
||||||
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
@@ -216,6 +217,7 @@
|
|||||||
--cal-border: #D1D5DB;
|
--cal-border: #D1D5DB;
|
||||||
--cal-border-subtle:#E5E7EB;
|
--cal-border-subtle:#E5E7EB;
|
||||||
--cal-border-muted:#F3F4F6;
|
--cal-border-muted:#F3F4F6;
|
||||||
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #111827;
|
--cal-text-emphasis: #111827;
|
||||||
@@ -235,6 +237,7 @@
|
|||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand:#111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis:#101010;
|
||||||
|
--cal-brand-text: white;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
/* background */
|
/* background */
|
||||||
@@ -250,6 +253,7 @@
|
|||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #E2FBE8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #FCEED8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #F9E3E2;
|
||||||
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
--cal-border: #444444;
|
--cal-border: #444444;
|
||||||
--cal-border-subtle: #2B2B2B;
|
--cal-border-subtle: #2B2B2B;
|
||||||
--cal-border-muted: #1C1C1C;
|
--cal-border-muted: #1C1C1C;
|
||||||
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #F3F4F6;
|
--cal-text-emphasis: #F3F4F6;
|
||||||
@@ -274,7 +279,8 @@
|
|||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand: #111827;
|
--cal-brand: white;
|
||||||
--cal-brand-emphasis: #101010;
|
--cal-brand-emphasis: #e1e1e1;
|
||||||
|
--cal-brand-text: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export const SnippedGenerator = {
|
|||||||
// extend some internals to gain information about current path, method and spec in the generator function metioned later
|
// extend some internals to gain information about current path, method and spec in the generator function metioned later
|
||||||
spec: {
|
spec: {
|
||||||
wrapSelectors: {
|
wrapSelectors: {
|
||||||
requestFor: (ori, system) => (state, path, method) => {
|
requestFor: (ori) => (state, path, method) => {
|
||||||
return ori(path, method)
|
return ori(path, method)
|
||||||
?.set("spec", state.get("json", {}))
|
?.set("spec", state.get("json", {}))
|
||||||
?.setIn(["oasPathMethod", "path"], path)
|
?.setIn(["oasPathMethod", "path"], path)
|
||||||
|
|||||||
@@ -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.2.1",
|
"next": "^13.4.6",
|
||||||
"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",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# @calcom/web
|
# @calcom/web
|
||||||
|
|
||||||
|
## 2.9.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @calcom/embed-snippet@1.0.9
|
||||||
|
- @calcom/embed-react@1.1.1
|
||||||
|
- @calcom/embed-core@1.2.1
|
||||||
|
|
||||||
|
## 2.9.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @calcom/embed-react@1.1.0
|
||||||
|
- @calcom/embed-core@1.2.0
|
||||||
|
- @calcom/embed-snippet@1.0.8
|
||||||
|
|
||||||
## 2.7.16
|
## 2.7.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
timeoutRef.current = null;
|
timeoutRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+123
-24
@@ -8,8 +8,11 @@ import { createRef, forwardRef, useRef, useState } from "react";
|
|||||||
import type { ControlProps } from "react-select";
|
import type { ControlProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
|
||||||
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||||
|
import { APP_NAME, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -35,18 +38,27 @@ const enum Theme {
|
|||||||
dark = "dark",
|
dark = "dark",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMBED_CAL_ORIGIN = WEBAPP_URL;
|
||||||
|
const EMBED_CAL_JS_URL = `${WEBAPP_URL}/embed/embed.js`;
|
||||||
|
|
||||||
type PreviewState = {
|
type PreviewState = {
|
||||||
inline: {
|
inline: {
|
||||||
width: string;
|
width: string;
|
||||||
height: string;
|
height: string;
|
||||||
};
|
};
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
floatingPopup: Record<string, string>;
|
floatingPopup: {
|
||||||
|
config?: {
|
||||||
|
layout: BookerLayouts;
|
||||||
|
};
|
||||||
|
[key: string]: string | boolean | undefined | Record<string, string>;
|
||||||
|
};
|
||||||
elementClick: Record<string, string>;
|
elementClick: Record<string, string>;
|
||||||
palette: {
|
palette: {
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout: BookerLayouts;
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
@@ -129,11 +141,13 @@ const getEmbedUIInstructionString = ({
|
|||||||
theme,
|
theme,
|
||||||
brandColor,
|
brandColor,
|
||||||
hideEventTypeDetails,
|
hideEventTypeDetails,
|
||||||
|
layout,
|
||||||
}: {
|
}: {
|
||||||
apiName: string;
|
apiName: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout?: string;
|
||||||
}) => {
|
}) => {
|
||||||
theme = theme !== "auto" ? theme : undefined;
|
theme = theme !== "auto" ? theme : undefined;
|
||||||
return getInstructionString({
|
return getInstructionString({
|
||||||
@@ -147,6 +161,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
hideEventTypeDetails: hideEventTypeDetails,
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
|
layout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -168,14 +183,24 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
|||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
export default function MyApp() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi();
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
return <Cal calLink="${calLink}" style={{width:"${width}",height:"${height}",overflow:"scroll"}} />;
|
return <Cal
|
||||||
|
calLink="${calLink}"
|
||||||
|
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
|
||||||
|
${previewState.layout ? "config={{layout: '" + previewState.layout + "'}}" : ""}${
|
||||||
|
IS_SELF_HOSTED
|
||||||
|
? `
|
||||||
|
calOrigin="${EMBED_CAL_ORIGIN}"
|
||||||
|
calJsUrl="${EMBED_CAL_JS_URL}"`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>;
|
||||||
};`;
|
};`;
|
||||||
},
|
},
|
||||||
"floating-popup": ({
|
"floating-popup": ({
|
||||||
@@ -186,38 +211,60 @@ function MyComponent() {
|
|||||||
uiInstructionCode: string;
|
uiInstructionCode: string;
|
||||||
}) => {
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import { getCalApi } from "@calcom/embed-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
export default function App() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""});
|
||||||
cal("floatingButton", ${floatingButtonArg});
|
cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
};`;
|
};`;
|
||||||
},
|
},
|
||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({
|
||||||
|
calLink,
|
||||||
|
uiInstructionCode,
|
||||||
|
previewState,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
uiInstructionCode: string;
|
||||||
|
previewState: PreviewState;
|
||||||
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import { getCalApi } from "@calcom/embed-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
export default function App() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
return <button data-cal-link="${calLink}" />;
|
return <button
|
||||||
|
data-cal-link="${calLink}"${IS_SELF_HOSTED ? `\ndata-cal-origin="${EMBED_CAL_ORIGIN}"` : ""}
|
||||||
|
${`data-cal-config='${JSON.stringify({
|
||||||
|
layout: previewState.layout,
|
||||||
|
})}'`}
|
||||||
|
>Click me</button>;
|
||||||
};`;
|
};`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HTML: {
|
HTML: {
|
||||||
inline: ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
inline: ({
|
||||||
|
calLink,
|
||||||
|
uiInstructionCode,
|
||||||
|
previewState,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
uiInstructionCode: string;
|
||||||
|
previewState: PreviewState;
|
||||||
|
}) => {
|
||||||
return code`Cal("inline", {
|
return code`Cal("inline", {
|
||||||
elementOrSelector:"#my-cal-inline",
|
elementOrSelector:"#my-cal-inline",
|
||||||
calLink: "${calLink}"
|
calLink: "${calLink}",
|
||||||
|
layout: "${previewState.layout}"
|
||||||
});
|
});
|
||||||
|
|
||||||
${uiInstructionCode}`;
|
${uiInstructionCode}`;
|
||||||
@@ -233,8 +280,22 @@ ${uiInstructionCode}`;
|
|||||||
return code`Cal("floatingButton", ${floatingButtonArg});
|
return code`Cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}`;
|
${uiInstructionCode}`;
|
||||||
},
|
},
|
||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({
|
||||||
return code`// Important: Make sure to add \`data-cal-link="${calLink}"\` attribute to the element you want to open Cal on click
|
calLink,
|
||||||
|
uiInstructionCode,
|
||||||
|
previewState,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
uiInstructionCode: string;
|
||||||
|
previewState: PreviewState;
|
||||||
|
}) => {
|
||||||
|
return code`
|
||||||
|
// Important: Please add following attributes to the element you want to open Cal on click
|
||||||
|
// \`data-cal-link="${calLink}"\`
|
||||||
|
// \`data-cal-config='${JSON.stringify({
|
||||||
|
layout: previewState.layout,
|
||||||
|
})}'\`
|
||||||
|
|
||||||
${uiInstructionCode}`;
|
${uiInstructionCode}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -260,6 +321,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: PreviewState["theme"];
|
theme: PreviewState["theme"];
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout?: BookerLayout;
|
||||||
};
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
@@ -267,6 +329,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
@@ -274,6 +337,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!frameworkCodes[embedType]) {
|
if (!frameworkCodes[embedType]) {
|
||||||
@@ -288,6 +352,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
} else if (embedType === "floating-popup") {
|
} else if (embedType === "floating-popup") {
|
||||||
const floatingButtonArg = {
|
const floatingButtonArg = {
|
||||||
calLink,
|
calLink,
|
||||||
|
...(IS_SELF_HOSTED ? { calOrigin: EMBED_CAL_ORIGIN } : null),
|
||||||
...previewState.floatingPopup,
|
...previewState.floatingPopup,
|
||||||
};
|
};
|
||||||
return frameworkCodes[embedType]({
|
return frameworkCodes[embedType]({
|
||||||
@@ -626,6 +691,7 @@ const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme;
|
|||||||
const ChooseEmbedTypesDialogContent = () => {
|
const ChooseEmbedTypesDialogContent = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@@ -671,6 +737,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const flags = useFlagMap();
|
||||||
|
const isBookerLayoutsEnabled = flags["booker-layouts"] === true;
|
||||||
|
|
||||||
const s = (href: string) => {
|
const s = (href: string) => {
|
||||||
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||||
@@ -691,12 +759,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
const [previewState, setPreviewState] = useState({
|
const [previewState, setPreviewState] = useState<PreviewState>({
|
||||||
inline: {
|
inline: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
theme: Theme.auto,
|
theme: Theme.auto,
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
floatingPopup: {},
|
floatingPopup: {},
|
||||||
elementClick: {},
|
elementClick: {},
|
||||||
hideEventTypeDetails: false,
|
hideEventTypeDetails: false,
|
||||||
@@ -764,6 +833,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
name: "ui",
|
name: "ui",
|
||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
|
layout: previewState.layout,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
@@ -798,6 +868,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const layoutOptions = [
|
||||||
|
{ value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") },
|
||||||
|
{ value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") },
|
||||||
|
{ value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") },
|
||||||
|
];
|
||||||
|
|
||||||
const FloatingPopupPositionOptions = [
|
const FloatingPopupPositionOptions = [
|
||||||
{
|
{
|
||||||
value: "bottom-right",
|
value: "bottom-right",
|
||||||
@@ -979,7 +1055,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
<div className="whitespace-nowrap">Text color</div>
|
<div className="whitespace-nowrap">Text color</div>
|
||||||
<div className="mt-2 mb-6 w-40 xl:mt-0 xl:w-full">
|
<div className="mb-6 mt-2 w-40 xl:mt-0 xl:w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
className="w-[130px]"
|
className="w-[130px]"
|
||||||
popoverAlign="start"
|
popoverAlign="start"
|
||||||
@@ -1070,6 +1146,34 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
))}
|
))}
|
||||||
|
{isBookerLayoutsEnabled && (
|
||||||
|
<Label className="mb-6">
|
||||||
|
<div className="mb-2">{t("layout")}</div>
|
||||||
|
<Select
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={layoutOptions[0]}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
const config = {
|
||||||
|
...(previewState.floatingPopup.config ?? {}),
|
||||||
|
layout: option.value,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
floatingPopup: {
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
layout: option.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={layoutOptions}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -1133,12 +1237,7 @@ export const EmbedDialog = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const embedUrl: string = router.query.embedUrl as string;
|
const embedUrl: string = router.query.embedUrl as string;
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
||||||
name="embed"
|
|
||||||
clearQueryParamsOnClose={queryParamsForDialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) window.resetEmbedStatus();
|
|
||||||
}}>
|
|
||||||
{!router.query.embedType ? (
|
{!router.query.embedType ? (
|
||||||
<ChooseEmbedTypesDialogContent />
|
<ChooseEmbedTypesDialogContent />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import dynamic from "next/dynamic";
|
|
||||||
import type { Dispatch } from "react";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { JSONObject } from "superjson/dist/types";
|
|
||||||
|
|
||||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
|
||||||
|
|
||||||
export type GateState = {
|
|
||||||
rainbowToken?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GateProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
gates: Gate[];
|
|
||||||
appData: JSONObject;
|
|
||||||
dispatch: Dispatch<Partial<GateState>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
|
|
||||||
|
|
||||||
// To add a new Gate just add the gate logic to the switch statement
|
|
||||||
const Gates: React.FC<GateProps> = ({ children, gates, appData, dispatch }) => {
|
|
||||||
const [rainbowToken, setRainbowToken] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch({ rainbowToken });
|
|
||||||
}, [rainbowToken, dispatch]);
|
|
||||||
|
|
||||||
let gateWrappers = <>{children}</>;
|
|
||||||
|
|
||||||
// Recursively wraps the `gateWrappers` with new gates allowing for multiple gates
|
|
||||||
for (const gate of gates) {
|
|
||||||
switch (gate) {
|
|
||||||
case "rainbow":
|
|
||||||
if (appData.blockchainId && appData.smartContractAddress && !rainbowToken) {
|
|
||||||
gateWrappers = (
|
|
||||||
<RainbowGate
|
|
||||||
setToken={setRainbowToken}
|
|
||||||
chainId={appData.blockchainId as number}
|
|
||||||
tokenAddress={appData.smartContractAddress as string}>
|
|
||||||
{gateWrappers}
|
|
||||||
</RainbowGate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gateWrappers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Gates;
|
|
||||||
@@ -71,7 +71,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
isCurrent
|
isCurrent
|
||||||
? "text-emphasis border-gray-900"
|
? "text-emphasis border-gray-900"
|
||||||
: "hover:border-default hover:text-default text-subtle border-transparent",
|
: "hover:border-default hover:text-default text-subtle border-transparent",
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
"group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
aria-current={isCurrent ? "page" : undefined}>
|
aria-current={isCurrent ? "page" : undefined}>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
import AppProviders from "@lib/app-providers";
|
import AppProviders from "@lib/app-providers";
|
||||||
@@ -13,7 +16,7 @@ import { seoConfig } from "@lib/config/next-seo.config";
|
|||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
export interface CalPageWrapper {
|
export interface CalPageWrapper {
|
||||||
(props?: any): JSX.Element;
|
(props?: AppProps): JSX.Element;
|
||||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +52,30 @@ function PageWrapper(props: AppProps) {
|
|||||||
};
|
};
|
||||||
// Use the layout defined at the page level, if available
|
// Use the layout defined at the page level, if available
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
|
// Canonical: Check if the URL is from cal.com so that we set the canonical conditionally
|
||||||
|
const isCalcom =
|
||||||
|
WEBAPP_URL &&
|
||||||
|
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") || new URL(WEBAPP_URL).hostname.endsWith("cal.dev"));
|
||||||
|
const path = router.asPath;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProviders {...providerProps}>
|
<AppProviders {...providerProps}>
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
<Head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<DefaultSeo
|
||||||
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
|
canonical={
|
||||||
|
isCalcom
|
||||||
|
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
||||||
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
|
}
|
||||||
|
{...seoConfig.defaultNextSeo}
|
||||||
|
/>
|
||||||
<I18nLanguageHandler />
|
<I18nLanguageHandler />
|
||||||
<Script
|
<Script
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { FunctionComponent, SVGProps } from "react";
|
||||||
|
|
||||||
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";
|
||||||
@@ -55,7 +57,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={data.type}
|
type={data.type}
|
||||||
render={(installProps) => {
|
render={(installProps) => {
|
||||||
const props = { ...installProps } as any;
|
const props = { ...installProps } as FunctionComponent<SVGProps<SVGSVGElement>>;
|
||||||
return (
|
return (
|
||||||
<DropdownItem {...props} color="minimal" type="button">
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
<span className="flex items-center gap-x-2">
|
<span className="flex items-center gap-x-2">
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const Component = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
{hasDescriptionItems && (
|
{hasDescriptionItems && (
|
||||||
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{descriptionItems ? (
|
{descriptionItems ? (
|
||||||
descriptionItems.map((descriptionItem, index) =>
|
descriptionItems.map((descriptionItem, index) =>
|
||||||
typeof descriptionItem === "object" ? (
|
typeof descriptionItem === "object" ? (
|
||||||
@@ -129,7 +129,10 @@ const Component = ({
|
|||||||
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
|
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
|
||||||
{categories[0]}
|
{categories[0]}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
• {t("published_by", { author })}
|
•{" "}
|
||||||
|
<a target="_blank" rel="noreferrer" href={website}>
|
||||||
|
{t("published_by", { author })}
|
||||||
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{isTemplate && (
|
{isTemplate && (
|
||||||
<Badge variant="red" className="mt-4">
|
<Badge variant="red" className="mt-4">
|
||||||
@@ -257,7 +260,7 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("learn_more")}</h4>
|
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
|
||||||
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
||||||
{docs && (
|
{docs && (
|
||||||
<li>
|
<li>
|
||||||
@@ -266,7 +269,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -278,7 +281,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -290,7 +293,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -303,7 +306,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -315,7 +318,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={AlertCircle}
|
Icon={AlertCircle}
|
||||||
headline={t("no_apps")}
|
headline={isAdmin ? t("no_apps") : t("no_apps_configured")}
|
||||||
description={isAdmin ? "You can enable apps in the settings" : ""}
|
description={isAdmin ? t("enable_in_settings") : t("please_contact_admin")}
|
||||||
buttonText={isAdmin ? t("apps_settings") : ""}
|
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||||
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,60 +2,12 @@ import type { ComponentProps } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
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 type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
|
||||||
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
|
||||||
{
|
|
||||||
name: "calendar",
|
|
||||||
href: "/apps/installed/calendar",
|
|
||||||
icon: Calendar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "conferencing",
|
|
||||||
href: "/apps/installed/conferencing",
|
|
||||||
icon: Video,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "payment",
|
|
||||||
href: "/apps/installed/payment",
|
|
||||||
icon: CreditCard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "automation",
|
|
||||||
href: "/apps/installed/automation",
|
|
||||||
icon: Share2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "analytics",
|
|
||||||
href: "/apps/installed/analytics",
|
|
||||||
icon: BarChart,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "other",
|
|
||||||
href: "/apps/installed/other",
|
|
||||||
icon: Grid,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function InstalledAppsLayout({
|
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 query = trpc.viewer.integrations.useQuery({
|
|
||||||
variant,
|
|
||||||
onlyInstalled: true,
|
|
||||||
});
|
|
||||||
let actualTabs = tabs;
|
|
||||||
if (query.data?.items.length === 0) {
|
|
||||||
actualTabs = tabs.filter((tab) => tab.name !== variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} hideHeadingOnMobile>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||||
|
|||||||
@@ -26,24 +26,24 @@ export default function TwoFactor({ center = true }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4">{t("2fa_code")}</Label>
|
||||||
|
|
||||||
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
|
||||||
|
<input type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
|
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<Input
|
{digits.map((digit, index) => (
|
||||||
className={className}
|
<Input
|
||||||
name="2fa1"
|
key={`2fa${index}`}
|
||||||
inputMode="decimal"
|
className={className}
|
||||||
{...digits[0]}
|
name={`2fa${index + 1}`}
|
||||||
autoFocus
|
inputMode="decimal"
|
||||||
autoComplete="one-time-code"
|
{...digit}
|
||||||
/>
|
autoFocus={index === 0}
|
||||||
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
|
autoComplete="one-time-code"
|
||||||
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
|
/>
|
||||||
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />
|
))}
|
||||||
<Input className={className} name="2fa5" inputMode="decimal" {...digits[4]} />
|
|
||||||
<Input className={className} name="2fa6" inputMode="decimal" {...digits[5]} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { getEventLocationType, getTranslatedLocation } 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 { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
@@ -23,20 +21,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
|
||||||
"attendee_in_person",
|
|
||||||
"in_person",
|
|
||||||
"attendee_phone_number",
|
|
||||||
"link_meeting",
|
|
||||||
"organizer_phone_number",
|
|
||||||
];
|
|
||||||
|
|
||||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
|
||||||
const translatedLocation = location.type.startsWith("integrations:")
|
|
||||||
? eventLocationType.label
|
|
||||||
: translateAbleKeys.includes(locationKey)
|
|
||||||
? t(locationKey)
|
|
||||||
: locationKey;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${location.type}-${index}`} 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">
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ type AvailableTimesProps = {
|
|||||||
bookingAttendees?: number | null;
|
bookingAttendees?: number | null;
|
||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
ethSignature?: string;
|
|
||||||
duration: number;
|
duration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
onTimeFormatChange,
|
onTimeFormatChange,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
ethSignature,
|
|
||||||
duration,
|
duration,
|
||||||
}) => {
|
}) => {
|
||||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
@@ -119,7 +117,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
timeFormat,
|
timeFormat,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +161,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => reserveSlot(slot)}
|
onClick={() => reserveSlot(slot)}
|
||||||
data-testid="time">
|
data-testid="time"
|
||||||
|
data-disabled="false">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && (
|
{!!seatsPerTimeSlot && (
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
|
<h2 className="text-default mb-2 mt-1 break-words text-sm font-medium ">
|
||||||
|
{eventType.team?.parent?.name} {profile.name}
|
||||||
|
</h2>
|
||||||
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const BookingDescriptionPayment = (props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{paymentAppData.paymentOption === "HOLD" ? (
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<>{props.t("no_show_fee_amount", params)}</>
|
<>{props.t("no_show_fee_amount", params)}</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
// For bookings with payment, only confirm if the booking is paid for
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
...((isPending && !paymentAppData.enabled) ||
|
||||||
|
(paymentAppData.enabled && !!paymentAppData.price && booking.paid)
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: "confirm",
|
id: "confirm",
|
||||||
@@ -318,6 +319,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
|
data-testid="rejection-confirm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
bookingConfirm(false);
|
bookingConfirm(false);
|
||||||
}}>
|
}}>
|
||||||
@@ -372,7 +374,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</td>
|
</td>
|
||||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||||
<div className="flex w-full items-center justify-between sm:hidden">
|
<div className="flex w-full items-center justify-between sm:hidden">
|
||||||
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||||
<div className="text-subtle pr-2 text-sm">
|
<div className="text-subtle pr-2 text-sm">
|
||||||
@@ -421,7 +423,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||||
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
<Badge className="me-2 ms-2 hidden sm:inline-flex" variant="orange">
|
||||||
{t("pending_payment")}
|
{t("pending_payment")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ export default function CancelBooking(props: Props) {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
data-testid="cancel_reason"
|
||||||
ref={cancelBookingRef}
|
ref={cancelBookingRef}
|
||||||
placeholder={t("cancellation_reason_placeholder")}
|
placeholder={t("cancellation_reason_placeholder")}
|
||||||
value={cancellationReason}
|
value={cancellationReason}
|
||||||
onChange={(e) => setCancellationReason(e.target.value)}
|
onChange={(e) => setCancellationReason(e.target.value)}
|
||||||
className="mt-2 mb-4 w-full "
|
className="mb-4 mt-2 w-full "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||||
@@ -75,7 +76,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-testid="cancel"
|
data-testid="confirm_cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
method: "DELETE",
|
method: "POST",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status >= 200 && res.status < 300) {
|
if (res.status >= 200 && res.status < 300) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function SkeletonItem() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
<div className="mt-4 hidden flex-shrink-0 sm:ml-5 sm:mt-0 lg:flex">
|
||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||||
<SkeletonText className="h-6 w-16" />
|
<SkeletonText className="h-6 w-16" />
|
||||||
<SkeletonText className="h-6 w-32" />
|
<SkeletonText className="h-6 w-32" />
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const SlotPicker = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
weekStart = 0,
|
weekStart = 0,
|
||||||
ethSignature,
|
|
||||||
}: {
|
}: {
|
||||||
eventType: Pick<
|
eventType: Pick<
|
||||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||||
@@ -94,7 +93,6 @@ export const SlotPicker = ({
|
|||||||
recurringEventCount?: number;
|
recurringEventCount?: number;
|
||||||
users: string[];
|
users: string[];
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
ethSignature?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
@@ -193,7 +191,6 @@ export const SlotPicker = ({
|
|||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
bookingAttendees={bookingAttendees}
|
bookingAttendees={bookingAttendees}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
ethSignature={ethSignature}
|
|
||||||
duration={parseInt(duration)}
|
duration={parseInt(duration)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
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 dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -27,8 +26,6 @@ import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
|
||||||
import Gates from "@components/Gates";
|
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import { SlotPicker } from "@components/booking/SlotPicker";
|
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||||
|
|
||||||
@@ -89,14 +86,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setIs24hClockInLocalStorage(is24Hours);
|
setIs24hClockInLocalStorage(is24Hours);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [gateState, gateDispatcher] = useReducer(
|
|
||||||
(state: GateState, newState: Partial<GateState>) => ({
|
|
||||||
...state,
|
|
||||||
...newState,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
@@ -124,20 +113,11 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
|
|
||||||
// Define conditional gates here
|
|
||||||
const gates = [
|
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
|
||||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
|
||||||
? ("rainbow" as Gate)
|
|
||||||
: undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
||||||
{
|
{
|
||||||
seatReferenceUid: rescheduleUid,
|
seatReferenceUid: rescheduleUid,
|
||||||
@@ -148,7 +128,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||||
@@ -211,9 +191,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="flex items-start text-sm font-medium">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<RefreshCcw className="float-left ml-[2px] mt-[7px] 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="-ml-2 mb-1 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -238,7 +218,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{paymentAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{paymentAppData.paymentOption === "HOLD" ? (
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<>
|
<>
|
||||||
{t("no_show_fee_amount", {
|
{t("no_show_fee_amount", {
|
||||||
@@ -296,7 +276,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||||
bookingAttendees={bookingAttendees || undefined}
|
bookingAttendees={bookingAttendees || undefined}
|
||||||
recurringEventCount={recurringEventCount}
|
recurringEventCount={recurringEventCount}
|
||||||
ethSignature={gateState.rainbowToken}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,7 +285,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm, useFormContext } from "react-hook-form";
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -12,7 +12,6 @@ import { z } from "zod";
|
|||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
@@ -51,8 +50,6 @@ import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/
|
|||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
|
||||||
import Gates from "@components/Gates";
|
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
|
|
||||||
import type { BookPageProps } from "../../../pages/[user]/book";
|
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||||
@@ -224,13 +221,6 @@ const BookingPage = ({
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [gateState, gateDispatcher] = useReducer(
|
|
||||||
(state: GateState, newState: Partial<GateState>) => ({
|
|
||||||
...state,
|
|
||||||
...newState,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { timezone } = useTimePreferences();
|
const { timezone } = useTimePreferences();
|
||||||
|
|
||||||
@@ -488,7 +478,6 @@ const BookingPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
ethSignature: gateState.rainbowToken,
|
|
||||||
}));
|
}));
|
||||||
recurringMutation.mutate(recurringBookings);
|
recurringMutation.mutate(recurringBookings);
|
||||||
} else {
|
} else {
|
||||||
@@ -506,25 +495,15 @@ const BookingPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
ethSignature: gateState.rainbowToken,
|
|
||||||
seatReferenceUid: router.query.seatReferenceUid as string,
|
seatReferenceUid: router.query.seatReferenceUid as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
|
||||||
|
|
||||||
// Define conditional gates here
|
|
||||||
const gates = [
|
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
|
||||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
|
||||||
? ("rainbow" as Gate)
|
|
||||||
: undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{rescheduleUid
|
{rescheduleUid
|
||||||
@@ -555,7 +534,7 @@ const BookingPage = ({
|
|||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:border-subtle text-default flex flex-col px-6 pb-0 pt-6 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
|
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
@@ -582,7 +561,7 @@ const BookingPage = ({
|
|||||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||||
return <p key={key}>{timeFormatted}</p>;
|
return <p key={key}>{timeFormatted}</p>;
|
||||||
})}
|
})}
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
{!rescheduleUid && eventType?.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
@@ -598,11 +577,11 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
{booking?.startTime && rescheduleUid && (
|
{booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
<p className="mb-2 mt-8 text-sm " data-testid="former_time_p">
|
||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="line-through ">
|
<p className="line-through ">
|
||||||
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDateTimeWithTimeZone(booking.startTime, i18n.language, timezone, {
|
parseDateTimeWithTimeZone(booking.startTime, i18n.language, timezone, {
|
||||||
@@ -686,7 +665,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,12 +57,15 @@ const LocationInput = (props: {
|
|||||||
<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") {
|
||||||
|
const { defaultValue, ...rest } = remainingProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
control={control}
|
control={control}
|
||||||
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
return <PhoneInput onChange={onChange} value={value} {...rest} />;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -88,6 +91,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
locationFormMethods.setValue("locationType", selection?.value);
|
locationFormMethods.setValue("locationType", selection?.value);
|
||||||
|
if (selection?.address) {
|
||||||
|
locationFormMethods.setValue("locationAddress", selection?.address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selection]);
|
}, [selection]);
|
||||||
@@ -149,12 +155,25 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
name: "locationType",
|
name: "locationType",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedAddrValue = useWatch({
|
||||||
|
control: locationFormMethods.control,
|
||||||
|
name: "locationAddress",
|
||||||
|
});
|
||||||
|
|
||||||
const eventLocationType = getEventLocationType(selectedLocation);
|
const eventLocationType = getEventLocationType(selectedLocation);
|
||||||
|
|
||||||
const defaultLocation = defaultValues?.find(
|
const defaultLocation = defaultValues?.find(
|
||||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
(location: { type: EventLocationType["type"]; address?: string }) => {
|
||||||
|
if (location.type === LocationType.InPerson) {
|
||||||
|
return location.type === eventLocationType?.type && location.address === selectedAddrValue;
|
||||||
|
} else {
|
||||||
|
return location.type === eventLocationType?.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(defaultLocation);
|
||||||
|
|
||||||
const LocationOptions = (() => {
|
const LocationOptions = (() => {
|
||||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||||
if (!eventLocationType.variable) {
|
if (!eventLocationType.variable) {
|
||||||
@@ -240,7 +259,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
{booking && (
|
{booking && (
|
||||||
<>
|
<>
|
||||||
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
|
<p className="text-emphasis mb-2 ml-1 mt-6 text-sm font-bold">{t("current_location")}:</p>
|
||||||
<p className="text-emphasis mb-2 ml-1 text-sm">
|
<p className="text-emphasis mb-2 ml-1 text-sm">
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
{getHumanReadableLocationValue(booking.location, t)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("send_reschedule_request")} />
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
|
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
|
||||||
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
|
<p className="text-emphasis mb-2 mt-6 text-sm font-bold">
|
||||||
{t("reason_for_reschedule_request")}
|
{t("reason_for_reschedule_request")}
|
||||||
<span className="text-subtle font-normal"> (Optional)</span>
|
<span className="text-subtle font-normal"> (Optional)</span>
|
||||||
</p>
|
</p>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="reschedule_reason"
|
data-testid="reschedule_reason"
|
||||||
name={t("reschedule_reason")}
|
name={t("reason_for_reschedule")}
|
||||||
value={rescheduleReason}
|
value={rescheduleReason}
|
||||||
onChange={(e) => setRescheduleReason(e.target.value)}
|
onChange={(e) => setRescheduleReason(e.target.value)}
|
||||||
className="mb-5 sm:mb-6"
|
className="mb-5 sm:mb-6"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
|
|||||||
{debugMap.map(([key, value]) => {
|
{debugMap.map(([key, value]) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<dt className="text-emphasis text-sm font-bold">{key}</dt>
|
<dt className="text-emphasis text-sm font-bold">{key}</dt>
|
||||||
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
|
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
|||||||
backgroundImage: "url(/calendar-preview.svg)",
|
backgroundImage: "url(/calendar-preview.svg)",
|
||||||
}}>
|
}}>
|
||||||
<div className="m-auto flex items-center justify-center self-stretch">
|
<div className="m-auto flex items-center justify-center self-stretch">
|
||||||
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
<div className="bg-subtle ml-11 mt-3 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
||||||
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
|
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
|
||||||
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
|
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
allowDisablingHostConfirmationEmails,
|
allowDisablingHostConfirmationEmails,
|
||||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
|
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -37,6 +38,7 @@ const generateHashedLink = (id: number) => {
|
|||||||
|
|
||||||
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 { data: user } = trpc.viewer.me.useQuery();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -48,6 +50,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const bookingFields: Prisma.JsonObject = {};
|
const bookingFields: Prisma.JsonObject = {};
|
||||||
|
|
||||||
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||||
|
const selectedThemeIsDark =
|
||||||
|
user?.theme === "dark" ||
|
||||||
|
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||||
|
|
||||||
eventType.bookingFields.forEach(({ name }) => {
|
eventType.bookingFields.forEach(({ name }) => {
|
||||||
bookingFields[name] = name + " input";
|
bookingFields[name] = name + " input";
|
||||||
@@ -65,7 +70,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
@@ -152,13 +157,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="edit custom name"
|
aria-label="edit custom name"
|
||||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}>
|
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||||
|
<div>
|
||||||
|
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} />
|
||||||
|
</div>
|
||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
title={t("booking_questions_title")}
|
title={t("booking_questions_title")}
|
||||||
@@ -211,7 +220,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
onChange(e ? value : "");
|
onChange(e ? value : "");
|
||||||
}}>
|
}}>
|
||||||
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||||
<div className="lg:-ml-2 lg:-mb-2">
|
<div className="lg:-mb-2 lg:-ml-2">
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
@@ -263,7 +272,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||||
aria-label="copy link"
|
aria-label="copy link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!shouldLockDisableProps("apps").disabled && (
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
<div>
|
<div className="bg-muted rounded-md p-8">
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
|
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{notInstalledApps?.map((app) => (
|
{notInstalledApps?.map((app) => (
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4 lg:space-y-8">
|
<div className="space-y-4 lg:space-y-8">
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="beforeBufferTime">
|
<Label htmlFor="beforeBufferTime">
|
||||||
{t("before_event")}
|
{t("before_event")}
|
||||||
@@ -245,7 +245,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="minimumBookingNotice">
|
<Label htmlFor="minimumBookingNotice">
|
||||||
{t("minimum_booking_notice")}
|
{t("minimum_booking_notice")}
|
||||||
@@ -416,7 +416,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.type === "RANGE" && (
|
{period.type === "RANGE" && (
|
||||||
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
|
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||||
<Controller
|
<Controller
|
||||||
name="periodDates"
|
name="periodDates"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -437,7 +437,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.suffix ? <span className="ms-2 me-2"> {period.suffix}</span> : null}
|
{period.suffix ? <span className="me-2 ms-2"> {period.suffix}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const EventSetupTab = (
|
|||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||||
|
|
||||||
const locationOptions = props.locationOptions.filter((option) => {
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
return !team ? option.label !== "Conferencing" : true;
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
@@ -136,9 +136,17 @@ export const EventSetupTab = (
|
|||||||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
const openLocationModal = (type: EventLocationType["type"], address = "") => {
|
||||||
const option = getLocationFromType(type, locationOptions);
|
const option = getLocationFromType(type, locationOptions);
|
||||||
setSelectedLocation(option);
|
if (option && option.value === LocationType.InPerson) {
|
||||||
|
const inPersonOption = {
|
||||||
|
...option,
|
||||||
|
address,
|
||||||
|
};
|
||||||
|
setSelectedLocation(inPersonOption);
|
||||||
|
} else {
|
||||||
|
setSelectedLocation(option);
|
||||||
|
}
|
||||||
setShowLocationModal(true);
|
setShowLocationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,7 +284,7 @@ export const EventSetupTab = (
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="border-default text-default mb-2 h-9 rounded-md border py-1.5 px-2 hover:cursor-pointer">
|
className="border-default text-default mb-2 h-9 rounded-md border px-2 py-1.5 hover:cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
@@ -290,7 +298,7 @@ export const EventSetupTab = (
|
|||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
<span className="ms-1 line-clamp-1 text-sm">{eventLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -298,10 +306,14 @@ export const EventSetupTab = (
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
locationFormMethods.setValue("locationType", location.type);
|
locationFormMethods.setValue("locationType", location.type);
|
||||||
locationFormMethods.unregister("locationLink");
|
locationFormMethods.unregister("locationLink");
|
||||||
locationFormMethods.unregister("locationAddress");
|
if (location.type === LocationType.InPerson) {
|
||||||
|
locationFormMethods.setValue("locationAddress", location.address);
|
||||||
|
} else {
|
||||||
|
locationFormMethods.unregister("locationAddress");
|
||||||
|
}
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
locationFormMethods.unregister("locationPhoneNumber");
|
||||||
setEditingLocationType(location.type);
|
setEditingLocationType(location.type);
|
||||||
openLocationModal(location.type);
|
openLocationModal(location.type, location.address);
|
||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||||
@@ -320,7 +332,7 @@ export const EventSetupTab = (
|
|||||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
) && (
|
) && (
|
||||||
<div className="text-default flex text-sm">
|
<div className="text-default flex text-sm">
|
||||||
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<Check className="mr-1.5 mt-0.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.
|
||||||
@@ -517,7 +529,18 @@ export const EventSetupTab = (
|
|||||||
defaultValues={formMethods.getValues("locations")}
|
defaultValues={formMethods.getValues("locations")}
|
||||||
selection={
|
selection={
|
||||||
selectedLocation
|
selectedLocation
|
||||||
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
|
? selectedLocation.address
|
||||||
|
? {
|
||||||
|
value: selectedLocation.value,
|
||||||
|
label: t(selectedLocation.label),
|
||||||
|
icon: selectedLocation.icon,
|
||||||
|
address: selectedLocation.address,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
value: selectedLocation.value,
|
||||||
|
label: t(selectedLocation.label),
|
||||||
|
icon: selectedLocation.icon,
|
||||||
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
setSelectedLocation={setSelectedLocation}
|
setSelectedLocation={setSelectedLocation}
|
||||||
|
|||||||
@@ -75,8 +75,7 @@ function getNavigation(props: {
|
|||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
availability: AvailabilityOption | undefined;
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||||
props;
|
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -178,6 +177,7 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
availability,
|
availability,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.splice(1, 0, {
|
navigation.splice(1, 0, {
|
||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
@@ -185,7 +185,7 @@ function EventTypeSingleLayout({
|
|||||||
info:
|
info:
|
||||||
isManagedEventType || isChildrenManagedEventType
|
isManagedEventType || isChildrenManagedEventType
|
||||||
? eventType.schedule === null
|
? eventType.schedule === null
|
||||||
? "Member's default schedule"
|
? "members_default_schedule"
|
||||||
: isChildrenManagedEventType
|
: isChildrenManagedEventType
|
||||||
? `${
|
? `${
|
||||||
eventType.scheduleName
|
eventType.scheduleName
|
||||||
@@ -193,7 +193,7 @@ function EventTypeSingleLayout({
|
|||||||
: `default_schedule_name`
|
: `default_schedule_name`
|
||||||
}`
|
}`
|
||||||
: eventType.scheduleName ?? `default_schedule_name`
|
: eventType.scheduleName ?? `default_schedule_name`
|
||||||
: `default_schedule_name`,
|
: eventType.scheduleName ?? `default_schedule_name`,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
@@ -220,7 +220,18 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
}, [
|
||||||
|
t,
|
||||||
|
eventType,
|
||||||
|
enabledAppsNumber,
|
||||||
|
installedAppsNumber,
|
||||||
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
|
isManagedEventType,
|
||||||
|
isChildrenManagedEventType,
|
||||||
|
team,
|
||||||
|
formMethods,
|
||||||
|
]);
|
||||||
|
|
||||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
@@ -236,22 +247,36 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{!eventType.metadata.managedEventConfig && (
|
{!eventType.metadata?.managedEventConfig && (
|
||||||
<>
|
<>
|
||||||
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 lg:flex">
|
<div
|
||||||
<Skeleton
|
className={classNames(
|
||||||
as={Label}
|
"sm:hover:bg-muted hidden cursor-pointer items-center rounded-md",
|
||||||
htmlFor="hiddenSwitch"
|
formMethods.watch("hidden") ? "px-2" : "",
|
||||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
"lg:flex"
|
||||||
{t("hide_from_profile")}
|
)}>
|
||||||
</Skeleton>
|
{formMethods.watch("hidden") && (
|
||||||
<Switch
|
<Skeleton
|
||||||
id="hiddenSwitch"
|
as={Label}
|
||||||
checked={formMethods.watch("hidden")}
|
htmlFor="hiddenSwitch"
|
||||||
onCheckedChange={(e) => {
|
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||||
formMethods.setValue("hidden", e);
|
{t("hidden")}
|
||||||
}}
|
</Skeleton>
|
||||||
/>
|
)}
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
formMethods.watch("hidden") ? t("show_eventtype_on_profile") : t("hide_from_profile")
|
||||||
|
}>
|
||||||
|
<div className="self-center rounded-md p-2">
|
||||||
|
<Switch
|
||||||
|
id="hiddenSwitch"
|
||||||
|
checked={!formMethods.watch("hidden")}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("hidden", !e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<VerticalDivider className="hidden lg:block" />
|
<VerticalDivider className="hidden lg:block" />
|
||||||
</>
|
</>
|
||||||
@@ -344,18 +369,18 @@ function EventTypeSingleLayout({
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="sm:hover:bg-subtle flex h-9 flex-row items-center justify-between py-2 px-4">
|
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
className="mt-2 inline cursor-pointer self-center pr-2 ">
|
className="mt-2 inline cursor-pointer self-center pr-2 ">
|
||||||
{t("hide_from_profile")}
|
{formMethods.watch("hidden") ? t("show_eventtype_on_profile") : t("hide_from_profile")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
checked={formMethods.watch("hidden")}
|
checked={!formMethods.watch("hidden")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hidden", e);
|
formMethods.setValue("hidden", !e);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,7 +390,7 @@ function EventTypeSingleLayout({
|
|||||||
<Button
|
<Button
|
||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={formMethods.formState.isSubmitting || isUpdateMutationLoading}
|
loading={isUpdateMutationLoading}
|
||||||
data-testid="update-eventtype"
|
data-testid="update-eventtype"
|
||||||
form="event-type-form">
|
form="event-type-form">
|
||||||
{t("save")}
|
{t("save")}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default function RequiresConfirmationController({
|
|||||||
if (!requiresConfirmation) {
|
if (!requiresConfirmation) {
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [requiresConfirmation]);
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
<Clock className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
<User className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface IConnectCalendarsProps {
|
|||||||
|
|
||||||
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||||
const { nextStep } = props;
|
const { nextStep } = props;
|
||||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery();
|
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||||
|
|
||||||
|
|||||||
@@ -8,23 +8,17 @@ import { md } from "@calcom/lib/markdownIt";
|
|||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { Avatar } from "@calcom/ui";
|
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
interface IUserProfileProps {
|
|
||||||
user: IOnboardingPageProps["user"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserProfile = (props: IUserProfileProps) => {
|
const UserProfile = () => {
|
||||||
const { user } = props;
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null);
|
||||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||||
defaultValues: { bio: user?.bio || "" },
|
defaultValues: { bio: user?.bio || "" },
|
||||||
});
|
});
|
||||||
@@ -76,7 +70,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
|
|
||||||
async function updateProfileHandler(event: FormEvent<HTMLFormElement>) {
|
async function updateProfileHandler(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const enteredAvatar = avatarRef.current.value;
|
const enteredAvatar = avatarRef.current?.value;
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
avatar: enteredAvatar,
|
avatar: enteredAvatar,
|
||||||
});
|
});
|
||||||
@@ -127,14 +121,16 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
buttonMsg={t("add_profile_photo")}
|
buttonMsg={t("add_profile_photo")}
|
||||||
handleAvatarChange={(newAvatar) => {
|
handleAvatarChange={(newAvatar) => {
|
||||||
avatarRef.current.value = newAvatar;
|
if (avatarRef.current) {
|
||||||
|
avatarRef.current.value = newAvatar;
|
||||||
|
}
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
window.HTMLInputElement.prototype,
|
window.HTMLInputElement.prototype,
|
||||||
"value"
|
"value"
|
||||||
)?.set;
|
)?.set;
|
||||||
nativeInputValueSetter?.call(avatarRef.current, newAvatar);
|
nativeInputValueSetter?.call(avatarRef.current, newAvatar);
|
||||||
const ev2 = new Event("input", { bubbles: true });
|
const ev2 = new Event("input", { bubbles: true });
|
||||||
avatarRef.current.dispatchEvent(ev2);
|
avatarRef.current?.dispatchEvent(ev2);
|
||||||
updateProfileHandler(ev2 as unknown as FormEvent<HTMLFormElement>);
|
updateProfileHandler(ev2 as unknown as FormEvent<HTMLFormElement>);
|
||||||
setImageSrc(newAvatar);
|
setImageSrc(newAvatar);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,15 +13,13 @@ import { ArrowRight } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
|
||||||
|
|
||||||
interface IUserSettingsProps {
|
interface IUserSettingsProps {
|
||||||
user: IOnboardingPageProps["user"];
|
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSettings = (props: IUserSettingsProps) => {
|
const UserSettings = (props: IUserSettingsProps) => {
|
||||||
const { user, nextStep } = props;
|
const { nextStep } = props;
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -69,7 +67,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Username textfield */}
|
{/* Username textfield */}
|
||||||
<UsernameAvailabilityField user={user} />
|
<UsernameAvailabilityField />
|
||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, Form, PasswordField } from "@calcom/ui";
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
@@ -75,14 +75,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
{...form.register("password")}
|
{...form.register("password")}
|
||||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||||
/>
|
/>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
|
||||||
|
|
||||||
<TwoFactor center={false} />
|
<TwoFactor center={false} />
|
||||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
<Button type="submit" className="me-2 ms-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||||
|
|||||||
@@ -178,19 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ms-2 me-2"
|
className="me-2 ms-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
<Button type="submit" className="me-2 ms-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
<Button type="submit" className="me-2 ms-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const TwoFactorModalHeader = ({ title, description }: { title: string; descripti
|
|||||||
<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">
|
||||||
<Shield className="text-inverted h-6 w-6" />
|
<Shield className="text-inverted h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||||
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, Form, PasswordField } from "@calcom/ui";
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
|
|
||||||
@@ -82,15 +82,13 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
{...form.register("password")}
|
{...form.register("password")}
|
||||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||||
/>
|
/>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
|
||||||
|
|
||||||
<TwoFactor center={false} />
|
<TwoFactor center={false} />
|
||||||
|
|
||||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
<Button type="submit" className="me-2 ms-2" data-testid="disable-2fa" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
<img src={dataUri} alt="" />
|
<img src={dataUri} alt="" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center font-mono text-xs">{secret}</p>
|
<p data-testid="two-factor-secret" className="text-center font-mono text-xs">
|
||||||
|
{secret}
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<Form handleSubmit={handleEnable} form={form}>
|
<Form handleSubmit={handleEnable} form={form}>
|
||||||
@@ -164,26 +166,34 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<TwoFactor center />
|
<TwoFactor center />
|
||||||
|
|
||||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
{errorMessage && (
|
||||||
|
<p data-testid="error-submitting-code" className="mt-1 text-sm text-red-700">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ms-2 me-2"
|
className="me-2 ms-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
data-testid="goto-otp-screen"
|
||||||
|
className="me-2 ms-2"
|
||||||
|
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
<Button type="submit" className="me-2 ms-2" data-testid="enable-2fa" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
|||||||
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
|
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
|
||||||
<span className="block">{t("username")}</span>
|
<span className="block">{t("username")}</span>
|
||||||
{longWebsiteUrl && (
|
{longWebsiteUrl && (
|
||||||
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 py-1 px-3">
|
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 px-3 py-1">
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const StepDone = (props: {
|
|||||||
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[420px] text-center">
|
<div className="max-w-[420px] text-center">
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
<h2 className="mb-1 mt-6 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
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 { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -23,20 +24,20 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||||
/>
|
/>
|
||||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
||||||
<p className="text-default font-medium">{member.name}</p>
|
<p className="text-default font-medium">{member.name}</p>
|
||||||
<div className="line-clamp-3 text-subtle overflow-ellipsis text-sm font-normal">
|
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
|
||||||
{!isBioEmpty ? (
|
{!isBioEmpty ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(markdownToSafeHTML(member.bio)) }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
|
<Edit2 className=" text-subtle group-hover:text-subtle -mt-px ml-1 inline h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
{...passThroughProps}
|
{...passThroughProps}
|
||||||
@@ -31,7 +31,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
value={value}
|
value={value}
|
||||||
required
|
required
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer truncate border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
"text-emphasis hover:text-default focus:text-emphasis absolute left-0 top-0 w-full cursor-pointer truncate border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
||||||
)}
|
)}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default function InfoBadge({ content }: { content: string }) {
|
|||||||
<>
|
<>
|
||||||
<Tooltip side="top" content={content}>
|
<Tooltip side="top" content={content}>
|
||||||
<span title={content}>
|
<span title={content}>
|
||||||
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
|
<Info className="text-subtle relative left-1 right-1 top-px mt-px h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loaded 100 of 833 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user