Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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,14 @@ 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
|
||||||
@@ -13,4 +13,4 @@ runs:
|
|||||||
- name: Set Environment Variables
|
- name: Set Environment Variables
|
||||||
uses: tw3lveparsecs/github-actions-setvars@latest
|
uses: tw3lveparsecs/github-actions-setvars@latest
|
||||||
with:
|
with:
|
||||||
envFilePath: gh.env
|
envFilePath: 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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -36,14 +36,14 @@ env:
|
|||||||
|
|
||||||
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:
|
||||||
@@ -66,6 +67,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 +108,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,21 @@
|
|||||||
|
name: "Validate PRs"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)_
|
||||||
|
|
||||||
@@ -201,6 +201,14 @@ 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
|
||||||
```
|
```
|
||||||
@@ -416,7 +424,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
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require("@calcom/config/eslint-preset");
|
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +26,6 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* required:
|
* required:
|
||||||
* - eventTypeId
|
* - eventTypeId
|
||||||
* - start
|
* - start
|
||||||
* - end
|
|
||||||
* - name
|
* - name
|
||||||
* - email
|
* - email
|
||||||
* - timeZone
|
* - timeZone
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
@@ -50,6 +50,6 @@
|
|||||||
"storybook-react-i18next": "^1.1.2",
|
"storybook-react-i18next": "^1.1.2",
|
||||||
"tailwindcss": "^3.2.1",
|
"tailwindcss": "^3.2.1",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^2.9.15"
|
"vite": "^4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@
|
|||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand:#111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis:#101010;
|
||||||
|
--cal-brand-text: white;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
/* background */
|
/* background */
|
||||||
@@ -274,7 +275,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)
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 type { BookerLayout } from "@calcom/features/bookings/Booker/types";
|
||||||
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -41,12 +44,13 @@ type PreviewState = {
|
|||||||
height: string;
|
height: string;
|
||||||
};
|
};
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
floatingPopup: Record<string, string>;
|
floatingPopup: Record<string, string | boolean | undefined>;
|
||||||
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 +133,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 +153,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
hideEventTypeDetails: hideEventTypeDetails,
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
|
layout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -260,6 +267,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 +275,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 +283,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]) {
|
||||||
@@ -626,6 +636,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 +682,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,7 +704,7 @@ 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%",
|
||||||
@@ -703,6 +716,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
palette: {
|
palette: {
|
||||||
brandColor: "#000000",
|
brandColor: "#000000",
|
||||||
},
|
},
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
@@ -765,6 +779,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
...previewState.palette,
|
...previewState.palette,
|
||||||
@@ -798,6 +813,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",
|
||||||
@@ -1070,6 +1091,27 @@ 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) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
layout: option.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={layoutOptions}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -1133,12 +1175,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;
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 mt-8 mb-2 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
}
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"in_person_attendee_address",
|
||||||
"in_person",
|
"in_person",
|
||||||
"attendee_phone_number",
|
"attendee_phone_number",
|
||||||
"link_meeting",
|
"link_meeting",
|
||||||
|
|||||||
@@ -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 mt-1 mb-2 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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ 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}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}`}
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
</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"
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -65,7 +66,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));
|
||||||
@@ -159,6 +160,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||||
|
<div>
|
||||||
|
<BookerLayoutSelector fallbackToUserSettings />
|
||||||
|
</div>
|
||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
title={t("booking_questions_title")}
|
title={t("booking_questions_title")}
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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">
|
||||||
@@ -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 py-2 px-4">
|
||||||
<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(
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
<Button type="submit" className="ms-2 me-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,7 +166,11 @@ 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">
|
||||||
@@ -178,12 +184,16 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
</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="ms-2 me-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="ms-2 me-2" data-testid="enable-2fa" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -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,7 +24,7 @@ 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 || ""}
|
||||||
@@ -36,7 +37,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
<>
|
<>
|
||||||
<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)) }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/ut
|
|||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { User } from "@calcom/prisma/client";
|
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
|
||||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
@@ -29,7 +27,6 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<User, "username" | "metadata">;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +54,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
readonly: disabled,
|
readonly: disabled,
|
||||||
user,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||||
const [markAsError, setMarkAsError] = useState(false);
|
const [markAsError, setMarkAsError] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface ICustomUsernameProps {
|
|||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UsernameTextfield = (props: ICustomUsernameProps) => {
|
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const {
|
const {
|
||||||
currentUsername,
|
currentUsername,
|
||||||
@@ -31,6 +31,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
usernameRef,
|
usernameRef,
|
||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||||
const [markAsError, setMarkAsError] = useState(false);
|
const [markAsError, setMarkAsError] = useState(false);
|
||||||
@@ -79,7 +80,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
<div className="ms-2 me-2 flex flex-row space-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
@@ -116,9 +117,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
addOnLeading={
|
|
||||||
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
|
||||||
}
|
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
@@ -133,6 +131,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
setInputUsernameValue(event.target.value);
|
setInputUsernameValue(event.target.value);
|
||||||
}}
|
}}
|
||||||
data-testid="username-input"
|
data-testid="username-input"
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||||
@@ -142,7 +141,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 hidden md:inline">
|
<div className="mt-7 hidden md:inline">
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { useRouter } from "next/router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import type { User } from "@calcom/prisma/client";
|
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
@@ -17,14 +19,24 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
|
|||||||
interface UsernameAvailabilityFieldProps {
|
interface UsernameAvailabilityFieldProps {
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<User, "username" | "metadata">;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
|
||||||
|
return organization
|
||||||
|
? organization.slug
|
||||||
|
? `${organization.slug}.${subdomainSuffix()}`
|
||||||
|
: organization.metadata && organization.metadata.requestedSlug
|
||||||
|
? `${organization.metadata.requestedSlug}.${subdomainSuffix()}`
|
||||||
|
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")
|
||||||
|
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||||
|
}
|
||||||
|
|
||||||
export const UsernameAvailabilityField = ({
|
export const UsernameAvailabilityField = ({
|
||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
user,
|
|
||||||
}: UsernameAvailabilityFieldProps) => {
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||||
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||||
const { username: currentUsername, setQuery: setCurrentUsername } =
|
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||||
@@ -37,6 +49,8 @@ export const UsernameAvailabilityField = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const usernamePrefix = useUserNamePrefix(user.organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -51,7 +65,7 @@ export const UsernameAvailabilityField = ({
|
|||||||
setInputUsernameValue={onChange}
|
setInputUsernameValue={onChange}
|
||||||
onSuccessMutation={onSuccessMutation}
|
onSuccessMutation={onSuccessMutation}
|
||||||
onErrorMutation={onErrorMutation}
|
onErrorMutation={onErrorMutation}
|
||||||
user={user}
|
addOnLeading={usernamePrefix}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ export const CheckedSelect = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
styles={{
|
|
||||||
option: (styles, { isDisabled }) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
name={props.name}
|
name={props.name}
|
||||||
placeholder={props.placeholder || t("select")}
|
placeholder={props.placeholder || t("select")}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type LocationOption = {
|
|||||||
value: EventLocationType["type"];
|
value: EventLocationType["type"];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
address?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
|||||||
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
||||||
return opts.empty(query);
|
return opts.empty(query);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return opts.success(query as any);
|
return opts.success(query as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +89,10 @@ const withQuery = <
|
|||||||
TInput = inferProcedureInput<TQuery>,
|
TInput = inferProcedureInput<TQuery>,
|
||||||
TOutput = inferProcedureOutput<TQuery>
|
TOutput = inferProcedureOutput<TQuery>
|
||||||
>(
|
>(
|
||||||
queryProcedure: DecorateProcedure<TQuery, any, any>,
|
queryProcedure: DecorateProcedure<TQuery, inferProcedureInput<TQuery>, inferProcedureOutput<TQuery>>,
|
||||||
|
|
||||||
input?: TInput,
|
input?: TInput,
|
||||||
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
|
params?: UseTRPCQueryOptions<TQuery, TInput, TOutput, TOutput, TError>
|
||||||
) => {
|
) => {
|
||||||
return function WithQuery(
|
return function WithQuery(
|
||||||
opts: Omit<
|
opts: Omit<
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { NextRouter } from "next/router";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||||
|
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||||
@@ -205,6 +207,11 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const orgBrand = useOrgBrandingValues();
|
||||||
|
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
const AppProviders = (props: AppPropsWithChildren) => {
|
const AppProviders = (props: AppPropsWithChildren) => {
|
||||||
const session = trpc.viewer.public.session.useQuery().data;
|
const session = trpc.viewer.public.session.useQuery().data;
|
||||||
// No need to have intercom on public pages - Good for Page Performance
|
// No need to have intercom on public pages - Good for Page Performance
|
||||||
@@ -222,7 +229,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
isThemeSupported={props.Component.isThemeSupported}
|
isThemeSupported={props.Component.isThemeSupported}
|
||||||
isBookingPage={props.Component.isBookingPage}>
|
isBookingPage={props.Component.isBookingPage}>
|
||||||
<FeatureFlagsProvider>
|
<FeatureFlagsProvider>
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<OrgBrandProvider>
|
||||||
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
|
</OrgBrandProvider>
|
||||||
</FeatureFlagsProvider>
|
</FeatureFlagsProvider>
|
||||||
</CalcomThemeProvider>
|
</CalcomThemeProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -5,7 +5,25 @@ export default function useRouterQuery<T extends string>(name: T) {
|
|||||||
const existingQueryParams = router.asPath.split("?")[1];
|
const existingQueryParams = router.asPath.split("?")[1];
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(existingQueryParams);
|
const urlParams = new URLSearchParams(existingQueryParams);
|
||||||
const query = Object.fromEntries(urlParams);
|
const query: Record<string, string | string[]> = {};
|
||||||
|
// Following error is thrown by Typescript:
|
||||||
|
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
|
||||||
|
// We should change the target to higher ES2019 atleast maybe
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
for (const [key, value] of urlParams) {
|
||||||
|
if (!query[key]) {
|
||||||
|
query[key] = value;
|
||||||
|
} else {
|
||||||
|
let queryValue = query[key];
|
||||||
|
if (queryValue instanceof Array) {
|
||||||
|
queryValue.push(value);
|
||||||
|
} else {
|
||||||
|
queryValue = query[key] = [queryValue];
|
||||||
|
queryValue.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setQuery = (newValue: string | number | null | undefined) => {
|
const setQuery = (newValue: string | number | null | undefined) => {
|
||||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
type GetSSRResult<TProps> =
|
type GetSSRResult<TProps> =
|
||||||
//
|
//
|
||||||
{ props: TProps } | { redirect: any } | { notFound: boolean };
|
{ props: TProps } | { redirect: { destination: string; permanent: boolean } } | { notFound: boolean };
|
||||||
|
|
||||||
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;
|
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
|||||||
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||||
const ssrResponse = await getServerSideProps(context);
|
const ssrResponse = await getServerSideProps(context);
|
||||||
const embed = context.query.embed;
|
const embed = context.query.embed;
|
||||||
|
const layout = context.query.layout;
|
||||||
|
|
||||||
if ("redirect" in ssrResponse) {
|
if ("redirect" in ssrResponse) {
|
||||||
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
||||||
@@ -18,6 +19,8 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
|||||||
destinationUrlObj.pathname +
|
destinationUrlObj.pathname +
|
||||||
"/embed?" +
|
"/embed?" +
|
||||||
destinationUrlObj.searchParams.toString() +
|
destinationUrlObj.searchParams.toString() +
|
||||||
|
"&layout=" +
|
||||||
|
layout +
|
||||||
"&embed=" +
|
"&embed=" +
|
||||||
embed;
|
embed;
|
||||||
|
|
||||||
|
|||||||
+41
-13
@@ -3,20 +3,34 @@ import { collectEvents } from "next-collect/server";
|
|||||||
import type { NextMiddleware } from "next/server";
|
import type { NextMiddleware } from "next/server";
|
||||||
import { NextResponse, userAgent } from "next/server";
|
import { NextResponse, userAgent } from "next/server";
|
||||||
|
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||||
|
|
||||||
let cold = true;
|
|
||||||
|
|
||||||
const middleware: NextMiddleware = async (req) => {
|
const middleware: NextMiddleware = async (req) => {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
const requestHeaders = new Headers(req.headers);
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.get("host") ?? "");
|
||||||
|
/**
|
||||||
|
* We are using env variable to toggle new-booker because using flags would be an unnecessary delay for booking pages
|
||||||
|
* Also, we can't easily identify the booker page requests here(to just fetch the flags for those requests)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Make sure we are in the presence of an organization
|
||||||
|
if (isValidOrgDomain && url.pathname === "/") {
|
||||||
|
// In the presence of an organization, cover its profile page at "/"
|
||||||
|
// rewrites for org profile page using team profile page
|
||||||
|
url.pathname = `/org/${currentOrgDomain}`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
|
||||||
|
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
|
||||||
|
req.nextUrl.pathname = "/api/nope";
|
||||||
|
return NextResponse.redirect(req.nextUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// This console.log is required to create a report in axios for hot and cold requests
|
|
||||||
console.log(cold ? "Cold Start" : "Hot Start");
|
|
||||||
requestHeaders.set("x-cal-cold-start", cold ? "true" : "false");
|
|
||||||
cold = false;
|
|
||||||
if (!url.pathname.startsWith("/api")) {
|
if (!url.pathname.startsWith("/api")) {
|
||||||
//
|
//
|
||||||
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
||||||
@@ -54,13 +68,6 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that embed query param is there in when /embed is added.
|
|
||||||
// query param is the way in which client side code knows that it is in embed mode.
|
|
||||||
if (url.pathname.endsWith("/embed") && typeof url.searchParams.get("embed") !== "string") {
|
|
||||||
url.searchParams.set("embed", "");
|
|
||||||
return NextResponse.redirect(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't 404 old routing_forms links
|
// Don't 404 old routing_forms links
|
||||||
if (url.pathname.startsWith("/apps/routing_forms")) {
|
if (url.pathname.startsWith("/apps/routing_forms")) {
|
||||||
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
||||||
@@ -76,6 +83,26 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
requestHeaders.set("x-csp-enforce", "true");
|
requestHeaders.set("x-csp-enforce", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isValidOrgDomain) {
|
||||||
|
// Match /:slug to determine if it corresponds to org subteam slug or org user slug
|
||||||
|
const slugs = /^\/([^/]+)(\/[^/]+)?$/.exec(url.pathname);
|
||||||
|
// In the presence of an organization, if not team profile, a user or team is being accessed
|
||||||
|
if (slugs) {
|
||||||
|
const [_, teamName, eventType] = slugs;
|
||||||
|
// Fetch the corresponding subteams for the entered organization
|
||||||
|
const getSubteams = await fetch(`${WEBAPP_URL}/api/organizations/${currentOrgDomain}/subteams`);
|
||||||
|
if (getSubteams.ok) {
|
||||||
|
const data = await getSubteams.json();
|
||||||
|
// Treat entered slug as a team if found in the subteams fetched
|
||||||
|
if (data.slugs.includes(teamName)) {
|
||||||
|
// Rewriting towards /team/:slug to bring up the team profile within the org
|
||||||
|
url.pathname = `/team/${teamName}${eventType ?? ""}`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next({
|
return NextResponse.next({
|
||||||
request: {
|
request: {
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
@@ -85,6 +112,7 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
"/:path*",
|
||||||
"/api/collect-events/:path*",
|
"/api/collect-events/:path*",
|
||||||
"/api/auth/:path*",
|
"/api/auth/:path*",
|
||||||
"/apps/routing_forms/:path*",
|
"/apps/routing_forms/:path*",
|
||||||
|
|||||||
+104
-12
@@ -2,7 +2,7 @@ require("dotenv").config({ path: "../../.env" });
|
|||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
|
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
|
|
||||||
@@ -57,6 +57,20 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const informAboutDuplicateTranslations = () => {
|
||||||
|
const valueSet = new Set();
|
||||||
|
|
||||||
|
for (const key in englishTranslation) {
|
||||||
|
if (valueSet.has(englishTranslation[key])) {
|
||||||
|
console.warn("\x1b[33mDuplicate value found in:", "\x1b[0m", key);
|
||||||
|
} else {
|
||||||
|
valueSet.add(englishTranslation[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
informAboutDuplicateTranslations();
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
if (process.env.ANALYZE === "true") {
|
if (process.env.ANALYZE === "true") {
|
||||||
// only load dependency if env `ANALYZE` was set
|
// only load dependency if env `ANALYZE` was set
|
||||||
@@ -169,7 +183,16 @@ const nextConfig = {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
||||||
|
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
||||||
|
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
||||||
|
const userTypeRouteRegExp = `/:user((?!${pages.join("|")})[^/]*)/:type`;
|
||||||
|
|
||||||
|
let rewrites = [
|
||||||
|
{
|
||||||
|
source: "/org/:slug",
|
||||||
|
destination: "/team/:slug",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: "/:user/avatar.png",
|
source: "/:user/avatar.png",
|
||||||
destination: "/api/user/avatar?username=:user",
|
destination: "/api/user/avatar?username=:user",
|
||||||
@@ -201,21 +224,85 @@ const nextConfig = {
|
|||||||
source: "/cancel/:path*",
|
source: "/cancel/:path*",
|
||||||
destination: "/booking/:path*",
|
destination: "/booking/:path*",
|
||||||
},
|
},
|
||||||
|
// Keep cookie based booker enabled just in case we disable new-booker globally
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: userTypeRouteRegExp,
|
||||||
|
destination: "/new-booker/:user/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type",
|
||||||
|
destination: "/new-booker/team/:slug/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/d/:link/:slug",
|
||||||
|
destination: "/new-booker/d/:link/:slug",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Keep cookie based booker enabled to test new-booker embed in production
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||||
|
destination: "/new-booker/:user/:type/embed",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type/embed",
|
||||||
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
/* TODO: have these files being served from another deployment or CDN {
|
/* TODO: have these files being served from another deployment or CDN {
|
||||||
source: "/embed/embed.js",
|
source: "/embed/embed.js",
|
||||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||||
}, */
|
}, */
|
||||||
{
|
|
||||||
source: `/:user((?!${pages.join("|")}).*)/:type`,
|
/**
|
||||||
destination: "/new-booker/:user/:type",
|
* Enables new booker using cookie. It works even if NEW_BOOKER_ENABLED_FOR_NON_EMBED, NEW_BOOKER_ENABLED_FOR_EMBED are disabled
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
*/
|
||||||
},
|
|
||||||
{
|
|
||||||
source: "/team/:slug/:type",
|
|
||||||
destination: "/new-booker/team/:slug/:type",
|
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Enable New Booker for all Embed Requests
|
||||||
|
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
|
||||||
|
console.log("Enabling New Booker for Embed");
|
||||||
|
rewrites.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||||
|
destination: "/new-booker/:user/:type/embed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type/embed",
|
||||||
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable New Booker for All but embed Requests
|
||||||
|
if (process.env.NEW_BOOKER_ENABLED_FOR_NON_EMBED === "1") {
|
||||||
|
console.log("Enabling New Booker for Non-Embed");
|
||||||
|
rewrites.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: userTypeRouteRegExp,
|
||||||
|
destination: "/new-booker/:user/:type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type",
|
||||||
|
destination: "/new-booker/team/:slug/:type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/d/:link/:slug",
|
||||||
|
destination: "/new-booker/d/:link/:slug",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rewrites;
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
@@ -274,6 +361,11 @@ const nextConfig = {
|
|||||||
destination: "/teams",
|
destination: "/teams",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/settings/admin",
|
||||||
|
destination: "/settings/admin/flags",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
/* V2 testers get redirected to the new settings */
|
/* V2 testers get redirected to the new settings */
|
||||||
{
|
{
|
||||||
source: "/settings/profile",
|
source: "/settings/profile",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.9.3",
|
"version": "3.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
"@tremor/react": "^2.0.0",
|
"@tremor/react": "^2.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
|
"@upstash/ratelimit": "^0.4.3",
|
||||||
|
"@upstash/redis": "^1.21.0",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.5.0",
|
"@vercel/og": "^0.5.0",
|
||||||
@@ -109,6 +111,7 @@
|
|||||||
"react-intl": "^5.25.1",
|
"react-intl": "^5.25.1",
|
||||||
"react-live-chat-loader": "^2.7.3",
|
"react-live-chat-loader": "^2.7.3",
|
||||||
"react-multi-email": "^0.5.3",
|
"react-multi-email": "^0.5.3",
|
||||||
|
"react-phone-input-2": "^2.15.1",
|
||||||
"react-phone-number-input": "^3.2.7",
|
"react-phone-number-input": "^3.2.7",
|
||||||
"react-schemaorg": "^2.0.0",
|
"react-schemaorg": "^2.0.0",
|
||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
|
|||||||
+3
-47
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
|
import { BookOpen, Check, ChevronRight, FileText, Slack } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
@@ -165,29 +165,7 @@ export default function Custom404() {
|
|||||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||||
<svg
|
<Slack strokeWidth="1.5" fill="currentColor" className="h-6 w-6" />
|
||||||
viewBox="0 0 2447.6 2452.5"
|
|
||||||
className="h-6 w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipRule="evenodd" fillRule="evenodd">
|
|
||||||
<path
|
|
||||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -313,29 +291,7 @@ export default function Custom404() {
|
|||||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||||
<svg
|
<Slack strokeWidth="1.5" fill="currentColor" className="h-6 w-6" />
|
||||||
viewBox="0 0 2447.6 2452.5"
|
|
||||||
className="h-6 w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipRule="evenodd" fillRule="evenodd">
|
|
||||||
<path
|
|
||||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
|
||||||
fill="rgba(55, 65, 81)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
@@ -255,6 +256,7 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const crypto = await import("crypto");
|
const crypto = await import("crypto");
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
|
||||||
const usernameList = getUsernameList(context.query.user as string);
|
const usernameList = getUsernameList(context.query.user as string);
|
||||||
const dataFetchStart = Date.now();
|
const dataFetchStart = Date.now();
|
||||||
@@ -263,6 +265,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
username: {
|
username: {
|
||||||
in: usernameList,
|
in: usernameList,
|
||||||
},
|
},
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -272,6 +279,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
bio: true,
|
bio: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
|
organizationId: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
@@ -284,7 +292,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!users.length) {
|
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
} as {
|
} as {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
@@ -15,7 +17,9 @@ import type { EmbedProps } from "@lib/withEmbedSsr";
|
|||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||||
|
|
||||||
export default function Type(props: AvailabilityPageProps) {
|
export default function Type(props: AvailabilityPageProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -50,6 +54,21 @@ export default function Type(props: AvailabilityPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
) : !props.isValidOrgDomain && props.organizationContext ? (
|
||||||
|
<div className="dark:bg-darkgray-50 h-screen">
|
||||||
|
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||||
|
<div className="space-y-6" data-testid="event-types">
|
||||||
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
|
<div className="text-muted dark:text-inverted p-8 text-center">
|
||||||
|
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
|
||||||
|
{" " + t("unavailable")}
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-md">{t("user_belongs_organization")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AvailabilityPage {...props} />
|
<AvailabilityPage {...props} />
|
||||||
);
|
);
|
||||||
@@ -59,21 +78,25 @@ Type.isBookingPage = true;
|
|||||||
Type.PageWrapper = PageWrapper;
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||||
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
|
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
const { type: slug, user: username } = paramsSchema.parse(context.query);
|
||||||
|
|
||||||
const { type: slug, user: username } = paramsSchema.parse(context.params);
|
const user = await prisma.user.findFirst({
|
||||||
const ssg = await ssgInit(context);
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
where: {
|
||||||
/** TODO: We should standarize this */
|
/** TODO: We should standarize this */
|
||||||
username: username.toLowerCase().replace(/( |%20)/g, "+"),
|
username: username.toLowerCase().replace(/( |%20)/g, "+"),
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -87,6 +110,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
organizationId: true,
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
where: {
|
where: {
|
||||||
// Many-to-many relationship causes inclusion of the team events - cool -
|
// Many-to-many relationship causes inclusion of the team events - cool -
|
||||||
@@ -108,6 +132,17 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
logo: true,
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
logo: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
@@ -179,16 +214,17 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
// Dynamic group has no theme preference right now. It uses system theme.
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
themeBasis: user.username,
|
themeBasis: user.username,
|
||||||
|
organizationContext: user?.organizationId !== null,
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
|
||||||
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
|
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
|
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
@@ -197,10 +233,10 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
|
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
|
||||||
"@calcom/prisma/zod-utils"
|
"@calcom/prisma/zod-utils"
|
||||||
);
|
);
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
const { getAppFromSlug } = await import("@calcom/app-store/utils");
|
const { getAppFromSlug } = await import("@calcom/app-store/utils");
|
||||||
|
|
||||||
const ssg = await ssgInit(context);
|
|
||||||
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
||||||
const usernameList = getUsernameList(userParam);
|
const usernameList = getUsernameList(userParam);
|
||||||
const length = parseInt(typeParam);
|
const length = parseInt(typeParam);
|
||||||
@@ -230,6 +266,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
organizationId: true,
|
||||||
away: true,
|
away: true,
|
||||||
schedules: {
|
schedules: {
|
||||||
select: {
|
select: {
|
||||||
@@ -313,24 +350,21 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
themeBasis: null,
|
themeBasis: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
organizationContext: !users.some((user) => user.organizationId === null),
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
|
||||||
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { user: userParam } = paramsSchema.parse(context.params);
|
const { user: userParam } = paramsSchema.parse(context.params);
|
||||||
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
||||||
const isDynamicGroup = userParam.includes("+");
|
const isDynamicGroup = getUsernameList(userParam).length > 1;
|
||||||
if (isDynamicGroup) {
|
if (isDynamicGroup) {
|
||||||
return await getDynamicGroupPageProps(context);
|
return await getDynamicGroupPageProps(context);
|
||||||
} else {
|
} else {
|
||||||
return await getUserPageProps(context);
|
return await getUserPageProps(context);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
return { paths: [], fallback: "blocking" };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import type { GetStaticPropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { getStaticProps as _getStaticProps } from "../[type]";
|
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||||
|
|
||||||
export { getStaticPaths } from "../[type]";
|
|
||||||
|
|
||||||
export { default } from "../[type]";
|
export { default } from "../[type]";
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const staticResponse = await _getStaticProps(context);
|
const ssrResponse = await _getServerSideProps(context);
|
||||||
if (staticResponse.notFound) {
|
if (ssrResponse.notFound) {
|
||||||
return staticResponse;
|
return ssrResponse;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...staticResponse,
|
...ssrResponse,
|
||||||
props: {
|
props: {
|
||||||
...staticResponse.props,
|
...ssrResponse.props,
|
||||||
isEmbed: true,
|
isEmbed: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { getStaticProps, getStaticPaths, default } from "./index";
|
||||||
+14
-13
@@ -3,22 +3,20 @@
|
|||||||
* caching system that NextJS uses SSG pages.
|
* caching system that NextJS uses SSG pages.
|
||||||
* TODO: Redirect to user profile on browser
|
* TODO: Redirect to user profile on browser
|
||||||
*/
|
*/
|
||||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getCachedResults } from "@calcom/core";
|
import getCalendarsEvents from "@calcom/core/getCalendarsEvents";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const CalendarCache = () => <div />;
|
|
||||||
|
|
||||||
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
||||||
export const getStaticProps: GetStaticProps<
|
export const getStaticProps: GetStaticProps<
|
||||||
{ results: Awaited<ReturnType<typeof getCachedResults>> },
|
{ results: Awaited<ReturnType<typeof getCalendarsEvents>> },
|
||||||
{ user: string }
|
{ user: string }
|
||||||
> = async (context) => {
|
> = async (context) => {
|
||||||
const { user: username, month } = paramsSchema.parse(context.params);
|
const { user: username, month } = paramsSchema.parse(context.params);
|
||||||
const userWithCredentials = await prisma.user.findUnique({
|
const userWithCredentials = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
},
|
},
|
||||||
@@ -29,16 +27,16 @@ export const getStaticProps: GetStaticProps<
|
|||||||
selectedCalendars: true,
|
selectedCalendars: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const startDate = (
|
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||||
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs.utc() : dayjs.utc(month, "YYYY-MM")
|
const startDate = dayjs.utc(month, "YYYY-MM").startOf("day").subtract(11, "hours").format();
|
||||||
).startOf("day");
|
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||||
const endDate = startDate.endOf("month");
|
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
|
||||||
try {
|
try {
|
||||||
const results = userWithCredentials?.credentials
|
const results = userWithCredentials?.credentials
|
||||||
? await getCachedResults(
|
? await getCalendarsEvents(
|
||||||
userWithCredentials?.credentials,
|
userWithCredentials?.credentials,
|
||||||
startDate.format(),
|
startDate,
|
||||||
endDate.format(),
|
endDate,
|
||||||
userWithCredentials?.selectedCalendars
|
userWithCredentials?.selectedCalendars
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
@@ -64,5 +62,8 @@ export const getStaticPaths: GetStaticPaths = () => {
|
|||||||
fallback: "blocking",
|
fallback: "blocking",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticProps>;
|
||||||
|
const CalendarCache = (props: Props) =>
|
||||||
|
process.env.NODE_ENV === "development" ? <pre>{JSON.stringify(props, null, " ")}</pre> : <div />;
|
||||||
|
|
||||||
export default CalendarCache;
|
export default CalendarCache;
|
||||||
@@ -40,9 +40,9 @@ class MyDocument extends Document<Props> {
|
|||||||
return (
|
return (
|
||||||
<Html lang={locale}>
|
<Html lang={locale}>
|
||||||
<Head nonce={nonce}>
|
<Head nonce={nonce}>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/api/logo?type=favicon-32" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/api/logo?type=favicon-16" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||||
|
|||||||
@@ -5,36 +5,48 @@ import { z } from "zod";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendPasswordResetEmail } from "@calcom/emails";
|
import { sendPasswordResetEmail } from "@calcom/emails";
|
||||||
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
||||||
import rateLimit from "@calcom/lib/rateLimit";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
|
import rateLimiter from "@calcom/lib/rateLimit";
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const limiter = rateLimit({
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
intervalInMs: 60 * 1000, // 1 minute
|
|
||||||
});
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||||
|
|
||||||
const email = z
|
const email = z
|
||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
.transform((val) => val.toLowerCase())
|
.transform((val) => val.toLowerCase())
|
||||||
.parse(req.body?.email);
|
.safeParse(req.body?.email);
|
||||||
|
|
||||||
const { isRateLimited } = limiter.check(10, email); // 10 requests per minute
|
if (!email.success) {
|
||||||
|
return res.status(400).json({ message: "email is required" });
|
||||||
if (isRateLimited) {
|
|
||||||
return res.status(429).json({ message: "Too Many Requests." });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method !== "POST") {
|
// fallback to email if ip is not present
|
||||||
return res.status(405).end();
|
let ip = (req.headers["x-real-ip"] as string) ?? email.data;
|
||||||
|
|
||||||
|
const forwardedFor = req.headers["x-forwarded-for"] as string;
|
||||||
|
if (!ip && forwardedFor) {
|
||||||
|
ip = forwardedFor?.split(",").at(0) ?? email.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10 requests per minute
|
||||||
|
|
||||||
|
const limiter = await rateLimiter();
|
||||||
|
const limit = await limiter({
|
||||||
|
identifier: ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!limit.success) {
|
||||||
|
throw new Error(ErrorCode.RateLimitExceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const maybeUser = await prisma.user.findUnique({
|
const maybeUser = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email: email.data,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
@@ -97,3 +109,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return res.status(500).json({ message: "Unable to create password reset request" });
|
return res.status(500).json({ message: "Unable to create password reset request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: handler }),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||||
|
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
const signupSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(7),
|
||||||
|
language: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return;
|
return;
|
||||||
@@ -17,7 +26,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
const { email, password } = data;
|
const { email, password, language } = signupSchema.parse(data);
|
||||||
|
|
||||||
const username = slugify(data.username);
|
const username = slugify(data.username);
|
||||||
const userEmail = email.toLowerCase();
|
const userEmail = email.toLowerCase();
|
||||||
|
|
||||||
@@ -26,16 +36,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEmail || !userEmail.includes("@")) {
|
|
||||||
res.status(422).json({ message: "Invalid email" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password || password.trim().length < 7) {
|
|
||||||
res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is an existingUser if the username matches
|
// There is an existingUser if the username matches
|
||||||
// OR if the email matches AND either the email is verified
|
// OR if the email matches AND either the email is verified
|
||||||
// or both username and password are set
|
// or both username and password are set
|
||||||
@@ -106,5 +106,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await sendEmailVerification({
|
||||||
|
email: userEmail,
|
||||||
|
username,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({ message: "Created user" });
|
res.status(201).json({ message: "Created user" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
const verifySchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { token } = verifySchema.parse(req.query);
|
||||||
|
|
||||||
|
const foundToken = await prisma.verificationToken.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundToken) {
|
||||||
|
return res.status(401).json({ message: "No token found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayjs(foundToken?.expires).isBefore(dayjs())) {
|
||||||
|
return res.status(401).json({ message: "Token expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: foundToken?.identifier,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete token from DB after it has been used
|
||||||
|
await prisma.verificationToken.delete({
|
||||||
|
where: {
|
||||||
|
id: foundToken?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCompletedOnboarding = user.completedOnboarding;
|
||||||
|
|
||||||
|
res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
|
||||||
|
}
|
||||||
@@ -60,9 +60,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("OrganizerRequestEmail", {
|
renderEmail("VerifyAccountEmail", {
|
||||||
calEvent: evt,
|
language: t,
|
||||||
attendee: evt.organizer,
|
user: {
|
||||||
|
name: "Pro Example",
|
||||||
|
email: "pro@example.com",
|
||||||
|
},
|
||||||
|
verificationEmailLink:
|
||||||
|
"http://localhost:3000/api/auth/verify-email?token=b91af0eee5a9a24a8d83a3d3d6a58c1606496e94ced589441649273c66100f5b",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
||||||
@@ -26,7 +27,7 @@ const decryptedSchema = z.object({
|
|||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
const { action, token, reason } = querySchema.parse(req.query);
|
const { action, token, reason } = querySchema.parse(req.query);
|
||||||
const { bookingUid } = decryptedSchema.parse(
|
const { bookingUid, userId } = decryptedSchema.parse(
|
||||||
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -34,10 +35,32 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||||||
where: { uid: bookingUid },
|
where: { uid: bookingUid },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
/** We shape the session as required by tRPC router */
|
||||||
|
async function sessionGetter() {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username: "" /* Not used in this context */,
|
||||||
|
role: UserPermissionRole.USER,
|
||||||
|
},
|
||||||
|
hasValidLicense: true,
|
||||||
|
expires: "" /* Not used in this context */,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @see https://trpc.io/docs/server-side-calls */
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
const ctx = await createContext({ req, res });
|
const ctx = await createContext({ req, res }, sessionGetter);
|
||||||
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
const caller = viewerRouter.createCaller({
|
||||||
|
...ctx,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
user: { ...user, locale: user?.locale ?? "en" },
|
||||||
|
});
|
||||||
await caller.bookings.confirm({
|
await caller.bookings.confirm({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
recurringEventId: booking.recurringEventId || undefined,
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
@@ -46,7 +69,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let message = "Error confirming booking";
|
let message = "Error confirming booking";
|
||||||
if (e instanceof TRPCError) message = e.message;
|
if (e instanceof TRPCError) message = (e as TRPCError).message;
|
||||||
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-17
@@ -1,7 +1,19 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED, LOGO, LOGO_ICON, WEBAPP_URL } from "@calcom/lib/constants";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
|
import {
|
||||||
|
ANDROID_CHROME_ICON_192,
|
||||||
|
ANDROID_CHROME_ICON_256,
|
||||||
|
APPLE_TOUCH_ICON,
|
||||||
|
FAVICON_16,
|
||||||
|
FAVICON_32,
|
||||||
|
IS_SELF_HOSTED,
|
||||||
|
LOGO,
|
||||||
|
LOGO_ICON,
|
||||||
|
MSTILE_ICON,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[api/logo]"] });
|
const log = logger.getChildLogger({ prefix: ["[api/logo]"] });
|
||||||
@@ -20,12 +32,80 @@ function extractSubdomainAndDomain(hostname: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logoApiSchema = z.object({
|
const logoApiSchema = z.object({
|
||||||
icon: z.coerce.boolean().optional(),
|
type: z.coerce.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const SYSTEM_SUBDOMAINS = ["console", "app", "www"];
|
const SYSTEM_SUBDOMAINS = ["console", "app", "www"];
|
||||||
|
|
||||||
async function getTeamLogos(subdomain: string) {
|
type LogoType =
|
||||||
|
| "logo"
|
||||||
|
| "icon"
|
||||||
|
| "favicon-16"
|
||||||
|
| "favicon-32"
|
||||||
|
| "apple-touch-icon"
|
||||||
|
| "mstile"
|
||||||
|
| "android-chrome-192"
|
||||||
|
| "android-chrome-256";
|
||||||
|
|
||||||
|
type LogoTypeDefinition = {
|
||||||
|
fallback: string;
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
source: "appLogo" | "appIconLogo";
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoDefinitions: Record<LogoType, LogoTypeDefinition> = {
|
||||||
|
logo: {
|
||||||
|
fallback: `${WEBAPP_URL}${LOGO}`,
|
||||||
|
source: "appLogo",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fallback: `${WEBAPP_URL}${LOGO_ICON}`,
|
||||||
|
source: "appIconLogo",
|
||||||
|
},
|
||||||
|
"favicon-16": {
|
||||||
|
fallback: `${WEBAPP_URL}${FAVICON_16}`,
|
||||||
|
w: 16,
|
||||||
|
h: 16,
|
||||||
|
source: "appIconLogo",
|
||||||
|
},
|
||||||
|
"favicon-32": {
|
||||||
|
fallback: `${WEBAPP_URL}${FAVICON_32}`,
|
||||||
|
w: 32,
|
||||||
|
h: 32,
|
||||||
|
source: "appIconLogo",
|
||||||
|
},
|
||||||
|
"apple-touch-icon": {
|
||||||
|
fallback: `${WEBAPP_URL}${APPLE_TOUCH_ICON}`,
|
||||||
|
w: 180,
|
||||||
|
h: 180,
|
||||||
|
source: "appLogo",
|
||||||
|
},
|
||||||
|
mstile: {
|
||||||
|
fallback: `${WEBAPP_URL}${MSTILE_ICON}`,
|
||||||
|
w: 150,
|
||||||
|
h: 150,
|
||||||
|
source: "appLogo",
|
||||||
|
},
|
||||||
|
"android-chrome-192": {
|
||||||
|
fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_192}`,
|
||||||
|
w: 192,
|
||||||
|
h: 192,
|
||||||
|
source: "appLogo",
|
||||||
|
},
|
||||||
|
"android-chrome-256": {
|
||||||
|
fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_256}`,
|
||||||
|
w: 256,
|
||||||
|
h: 256,
|
||||||
|
source: "appLogo",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValidLogoType(type: string): type is LogoType {
|
||||||
|
return type in logoDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
// if not cal.com
|
// if not cal.com
|
||||||
@@ -39,25 +119,31 @@ async function getTeamLogos(subdomain: string) {
|
|||||||
}
|
}
|
||||||
// load from DB
|
// load from DB
|
||||||
const { default: prisma } = await import("@calcom/prisma");
|
const { default: prisma } = await import("@calcom/prisma");
|
||||||
const team = await prisma.team.findUniqueOrThrow({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
slug: subdomain,
|
slug: subdomain,
|
||||||
|
...(isValidOrgDomain && {
|
||||||
|
metadata: {
|
||||||
|
path: ["isOrganization"],
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
appLogo: true,
|
appLogo: true,
|
||||||
appIconLogo: true,
|
appIconLogo: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// try to use team logos, otherwise default to LOGO/LOGO_ICON regardless
|
|
||||||
return {
|
return {
|
||||||
appLogo: team.appLogo || `${WEBAPP_URL}${LOGO}`,
|
appLogo: team?.appLogo,
|
||||||
appIconLogo: team.appIconLogo || `${WEBAPP_URL}${LOGO_ICON}`,
|
appIconLogo: team?.appIconLogo,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) log.debug(error.message);
|
if (error instanceof Error) log.debug(error.message);
|
||||||
return {
|
return {
|
||||||
appLogo: `${WEBAPP_URL}${LOGO}`,
|
appLogo: undefined,
|
||||||
appIconLogo: `${WEBAPP_URL}${LOGO_ICON}`,
|
appIconLogo: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +154,7 @@ async function getTeamLogos(subdomain: string) {
|
|||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const parsedQuery = logoApiSchema.parse(query);
|
const parsedQuery = logoApiSchema.parse(query);
|
||||||
|
const { isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
|
|
||||||
const hostname = req?.headers["host"];
|
const hostname = req?.headers["host"];
|
||||||
if (!hostname) throw new Error("No hostname");
|
if (!hostname) throw new Error("No hostname");
|
||||||
@@ -75,14 +162,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
if (!domains) throw new Error("No domains");
|
if (!domains) throw new Error("No domains");
|
||||||
|
|
||||||
const [subdomain] = domains;
|
const [subdomain] = domains;
|
||||||
const { appLogo, appIconLogo } = await getTeamLogos(subdomain);
|
const teamLogos = await getTeamLogos(subdomain, isValidOrgDomain);
|
||||||
|
|
||||||
const filteredLogo = parsedQuery?.icon ? appIconLogo : appLogo;
|
// Resolve all icon types to team logos, falling back to Cal.com defaults.
|
||||||
|
const type: LogoType = parsedQuery?.type && isValidLogoType(parsedQuery.type) ? parsedQuery.type : "logo";
|
||||||
|
const logoDefinition = logoDefinitions[type];
|
||||||
|
const filteredLogo = teamLogos[logoDefinition.source] ?? logoDefinition.fallback;
|
||||||
|
|
||||||
const response = await fetch(filteredLogo);
|
try {
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const response = await fetch(filteredLogo);
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
res.setHeader("Content-Type", response.headers.get("content-type") as string);
|
let buffer = Buffer.from(arrayBuffer);
|
||||||
res.setHeader("Cache-Control", "s-maxage=86400");
|
|
||||||
res.send(buffer);
|
// If we need to resize the team logos (via Next.js' built-in image processing)
|
||||||
|
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
|
||||||
|
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
|
||||||
|
buffer = await optimizeImage({
|
||||||
|
buffer,
|
||||||
|
contentType: detectContentType(buffer) ?? "image/jpeg",
|
||||||
|
quality: 100,
|
||||||
|
width: logoDefinition.w,
|
||||||
|
height: logoDefinition.h, // optional
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", response.headers.get("content-type") as string);
|
||||||
|
res.setHeader("Cache-Control", "s-maxage=86400");
|
||||||
|
res.send(buffer);
|
||||||
|
} catch (error) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.json({ error: "Failed fetching logo" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "@calcom/features/ee/organizations/api/subteams";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user