Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
605f9c61f3 | ||
|
|
564862369f | ||
|
|
be6cf4705c | ||
|
|
a2457ab619 | ||
|
|
867966dd75 | ||
|
|
08ad4c07ea | ||
|
|
78da57055e | ||
|
|
d5c3d9d967 | ||
|
|
aa0e9e6a02 | ||
|
|
cad97f1779 | ||
|
|
31ee689672 | ||
|
|
3edb6f5ffd | ||
|
|
a8825badec | ||
|
|
04c634ec4b | ||
|
|
8dd9b8d74b | ||
|
|
0365157638 | ||
|
|
a571d40958 | ||
|
|
69491d5700 | ||
|
|
20022e2cdd | ||
|
|
492858b45b | ||
|
|
b49ad8294b | ||
|
|
aa30ff7d1a | ||
|
|
cfa720a523 | ||
|
|
9ab196c30e | ||
|
|
47e13cd2fd | ||
|
|
74ab6d209e | ||
|
|
46f0b55a45 | ||
|
|
e4893c2218 | ||
|
|
fa17139e8f | ||
|
|
1cdc97dd37 | ||
|
|
f630e8af40 | ||
|
|
fe0745121b | ||
|
|
f6d7568d0a | ||
|
|
520e7fe036 | ||
|
|
1bf50e09cd | ||
|
|
d1ca7b4bdd | ||
|
|
4e2473531f | ||
|
|
efb9bf7116 | ||
|
|
49ee5d300b | ||
|
|
1512551903 | ||
|
|
1b459eb212 | ||
|
|
e4eb21bf8d | ||
|
|
79f7a16f62 | ||
|
|
7fc884912e | ||
|
|
20b3efbcb6 | ||
|
|
427b19238d | ||
|
|
696122a701 | ||
|
|
7d9c21cf9a | ||
|
|
eb93524f49 | ||
|
|
07fd3ad29f | ||
|
|
389633394b | ||
|
|
d27817ad51 | ||
|
|
7af23305eb | ||
|
|
db947591ad | ||
|
|
2ef4a05750 | ||
|
|
f1ac45ee19 | ||
|
|
03c9fc7952 | ||
|
|
de2a56ebcb | ||
|
|
0bae688da1 | ||
|
|
7967a65ade | ||
|
|
1cd96d4c25 | ||
|
|
19bc329377 | ||
|
|
0c29f2a135 | ||
|
|
a145de56ee | ||
|
|
d2352da54b | ||
|
|
5d7685f2c0 | ||
|
|
ee8b2e67f0 | ||
|
|
a0c9123ee7 | ||
|
|
131611e114 | ||
|
|
b3407e19b5 | ||
|
|
0d4b0847d4 | ||
|
|
5d8575abf4 | ||
|
|
631bf4d9fe | ||
|
|
8fd1d76227 | ||
|
|
22b189cb19 | ||
|
|
27c29c65e2 | ||
|
|
f0c7d7ff29 | ||
|
|
679965782e | ||
|
|
650ecc7a85 | ||
|
|
8d27dcdea3 | ||
|
|
6128be8860 | ||
|
|
13577a5dbe | ||
|
|
9ba13a3f3c | ||
|
|
9ac562cfba | ||
|
|
e1411b3fb2 | ||
|
|
0f89415ec3 | ||
|
|
c6ff3dca4a | ||
|
|
e001c0b88f | ||
|
|
5cbd334a42 | ||
|
|
470ef737b5 | ||
|
|
f3e8bed1cf | ||
|
|
750362c0cf | ||
|
|
3811dd564d | ||
|
|
99fe67b879 | ||
|
|
2685e8154f | ||
|
|
4308ea8d52 | ||
|
|
358aa8be2b | ||
|
|
b20c0df4e8 | ||
|
|
09af14b3f4 | ||
|
|
77df4117d0 | ||
|
|
84f7924215 | ||
|
|
c8554e6878 | ||
|
|
5993e0842c | ||
|
|
7179b09d7b | ||
|
|
325dc9805f | ||
|
|
0f6a678b74 |
No files matched your search
@@ -20,12 +20,17 @@
|
|||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
# - DAILY.CO VIDEO
|
# - DAILY.CO VIDEO
|
||||||
|
# Enables Cal Video. to get your key
|
||||||
|
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||||
|
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
|
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
|
||||||
|
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
|
||||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
# Needed to enable Google Calendar integration and Login with Google
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
|
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||||
GOOGLE_API_CREDENTIALS=
|
GOOGLE_API_CREDENTIALS=
|
||||||
|
|
||||||
# To enable Login with Google you need to:
|
# To enable Login with Google you need to:
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ CALCOM_LICENSE_KEY=
|
|||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
# Opt-in to a dedicated connection pool for Prisma using Prisma Data Proxy
|
|
||||||
# Cold boots will be faster and you will be able to scale your DB independently of your application.
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||||
PRISMA_GENERATE_DATAPROXY=false
|
# PRISMA_GENERATE_DATAPROXY=true
|
||||||
|
PRISMA_GENERATE_DATAPROXY=
|
||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
- [React](https://reactjs.org/?ref=cal.com)
|
- [React](https://reactjs.org/?ref=cal.com)
|
||||||
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
||||||
- [Prisma](https://prisma.io/?ref=cal.com)
|
- [Prisma](https://prisma.io/?ref=cal.com)
|
||||||
|
- [Daily.co](https://go.cal.com/daily)
|
||||||
|
|
||||||
## Stay Up-to-Date
|
## Stay Up-to-Date
|
||||||
|
|
||||||
@@ -432,7 +433,7 @@ following
|
|||||||
|
|
||||||
### Obtaining Daily API Credentials
|
### Obtaining Daily API Credentials
|
||||||
|
|
||||||
1. Open [Daily](https://www.daily.co/) and sign into your account.
|
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||||
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
3. Copy your API key.
|
3. Copy your API key.
|
||||||
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
||||||
|
|||||||
@@ -94,16 +94,6 @@ const DestinationCalendarSelector = ({
|
|||||||
styles={{
|
styles={{
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||||
option: (defaultStyles, state) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
backgroundColor: state.isSelected
|
|
||||||
? state.isFocused
|
|
||||||
? "var(--brand-color)"
|
|
||||||
: "var(--brand-color)"
|
|
||||||
: state.isFocused
|
|
||||||
? "var(--brand-color-dark-mode)"
|
|
||||||
: "var(--brand-text-color)",
|
|
||||||
}),
|
|
||||||
control: (defaultStyles) => {
|
control: (defaultStyles) => {
|
||||||
return {
|
return {
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
|
|||||||
@@ -327,10 +327,10 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
||||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
|
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
|
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
@@ -378,9 +378,9 @@ const embeds: {
|
|||||||
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
|
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
||||||
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
|
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -400,12 +400,12 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
|
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
||||||
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
@@ -426,62 +426,65 @@ const embeds: {
|
|||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
fill="white"
|
fill="white"
|
||||||
/>
|
/>
|
||||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
<g filter="url(#filter0_i_3223_14162)">
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
|
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
|
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
|
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
</g>
|
||||||
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
|
||||||
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<path
|
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
|
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
fill="#3E3E3E"
|
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
/>
|
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
|
||||||
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
|
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
|
||||||
|
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -629,9 +632,9 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<DialogContent type="creation" size="lg">
|
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||||
<div className="mb-4">
|
<div className="mb-2">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
@@ -641,7 +644,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{embeds.map((embed, index) => (
|
{embeds.map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
|
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -649,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
embedType: embed.type,
|
embedType: embed.type,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
<div className="order-none box-border flex-none rounded-md border border-solid bg-white">
|
||||||
{embed.illustration}
|
{embed.illustration}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
|
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div>
|
||||||
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import { noop } from "lodash";
|
||||||
import type { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
@@ -45,12 +46,12 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
images,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const hasImages = images && images.length > 0;
|
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
@@ -94,17 +95,25 @@ const Component = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
{hasImages && (
|
{hasDescriptionItems && (
|
||||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{images ? (
|
{descriptionItems ? (
|
||||||
images.map((img) => (
|
descriptionItems.map((descriptionItem, index) =>
|
||||||
<img
|
typeof descriptionItem === "object" ? (
|
||||||
key={img}
|
<div
|
||||||
src={img}
|
key={`iframe-${index}`}
|
||||||
alt={`Screenshot of app ${name}`}
|
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
|
||||||
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
<iframe allowFullScreen {...descriptionItem.iframe} />
|
||||||
/>
|
</div>
|
||||||
))
|
) : (
|
||||||
|
<img
|
||||||
|
key={descriptionItem}
|
||||||
|
src={descriptionItem}
|
||||||
|
alt={`Screenshot of app ${name}`}
|
||||||
|
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText />
|
<SkeletonText />
|
||||||
)}
|
)}
|
||||||
@@ -113,7 +122,7 @@ const Component = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||||
hasImages && "lg:ml-8"
|
hasDescriptionItems && "lg:ml-8"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 flex pt-4">
|
<div className="mb-8 flex pt-4">
|
||||||
<header>
|
<header>
|
||||||
@@ -354,7 +363,7 @@ export default function App(props: {
|
|||||||
privacy?: string;
|
privacy?: string;
|
||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
images?: string[];
|
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
|||||||
@@ -96,16 +96,6 @@ const DestinationCalendarSelector = ({
|
|||||||
styles={{
|
styles={{
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||||
option: (defaultStyles, state) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
backgroundColor: state.isSelected
|
|
||||||
? state.isFocused
|
|
||||||
? "var(--brand-color)"
|
|
||||||
: "var(--brand-color)"
|
|
||||||
: state.isFocused
|
|
||||||
? "var(--brand-color-dark-mode)"
|
|
||||||
: "var(--brand-text-color)",
|
|
||||||
}),
|
|
||||||
control: (defaultStyles) => {
|
control: (defaultStyles) => {
|
||||||
return {
|
return {
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slots.map((slot) => {
|
slots.map((slot) => {
|
||||||
type BookingURL = {
|
type BookingURL = {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
query: Record<string, string | number | string[] | undefined>;
|
query: Record<string, string | number | string[] | undefined | TimeFormat>;
|
||||||
};
|
};
|
||||||
const bookingUrl: BookingURL = {
|
const bookingUrl: BookingURL = {
|
||||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
@@ -103,6 +103,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||||
type: eventTypeId,
|
type: eventTypeId,
|
||||||
slug: eventTypeSlug,
|
slug: eventTypeSlug,
|
||||||
|
timeFormat,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="gray"
|
variant="gray"
|
||||||
size="lg"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
duration === dur.toString()
|
duration === dur.toString()
|
||||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||||
|
import "@calcom/dayjs/locales";
|
||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
@@ -46,7 +48,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
// Get user so we can determine 12/24 hour format preferences
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
const query = useMeQuery();
|
const query = useMeQuery();
|
||||||
const user = query.data;
|
const user = query.data;
|
||||||
const { t } = useLocale();
|
const {
|
||||||
|
t,
|
||||||
|
i18n: { language },
|
||||||
|
} = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
@@ -195,7 +200,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
const startTime = dayjs(booking.startTime)
|
||||||
|
.locale(language)
|
||||||
|
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||||
@@ -431,7 +438,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
// Get user so we can determine 12/24 hour format preferences
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
const query = useMeQuery();
|
const query = useMeQuery();
|
||||||
const user = query.data;
|
const user = query.data;
|
||||||
const { t } = useLocale();
|
const {
|
||||||
|
t,
|
||||||
|
i18n: { language },
|
||||||
|
} = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const recurringCount = recurringDates.filter((date) => {
|
const recurringCount = recurringDates.filter((date) => {
|
||||||
return (
|
return (
|
||||||
@@ -461,7 +471,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
||||||
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
||||||
{" - "}
|
{" - "}
|
||||||
{dayjs(aDate).format("D MMMM YYYY")}
|
{dayjs(aDate).locale(language).format("D MMMM YYYY")}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}>
|
})}>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex justify-center"
|
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -86,7 +86,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
|
|
||||||
const res = await fetch("/api/cancel", {
|
const res = await fetch("/api/cancel", {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: booking?.id,
|
uid: booking?.uid,
|
||||||
cancellationReason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
allRemainingBookings,
|
allRemainingBookings,
|
||||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime:
|
startTime: dayjs().startOf("day"),
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
duration,
|
duration,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import type { ITimezoneOption } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
import { TimezoneSelect } from "@calcom/ui";
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,8 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const query = useMeQuery();
|
|
||||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -28,59 +23,16 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
|
||||||
padding: "10px 12px !important",
|
|
||||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
|
||||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
control: () => ({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "transparent !important",
|
|
||||||
minWidth: "5rem",
|
|
||||||
height: "24px",
|
|
||||||
minHeight: "24px !important",
|
|
||||||
boxShadow: "none !important",
|
|
||||||
}),
|
|
||||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#a5a5a5 !important",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
menu: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
minWidth: "20rem",
|
|
||||||
margin: "8px 0 0 -20px",
|
|
||||||
}),
|
|
||||||
valueContainer: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
padding: "0 0 0 6px",
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
marginLeft: 4,
|
|
||||||
padding: 0,
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return !!selectedTimeZone ? (
|
return !!selectedTimeZone ? (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
classNames={{
|
||||||
|
singleValue: () => "dark:text-darkgray-600 text-gray-600",
|
||||||
|
menu: () => "!w-64 max-w-[90vw]",
|
||||||
|
}}
|
||||||
|
variant="minimal"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
className="flex h-6 text-sm font-medium"
|
|
||||||
styles={customStyles}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
nofollow: eventType.hidden,
|
nofollow: eventType.hidden,
|
||||||
noindex: eventType.hidden,
|
noindex: eventType.hidden,
|
||||||
}}
|
}}
|
||||||
|
isBrandingHidden={restProps.isBrandingHidden}
|
||||||
/>
|
/>
|
||||||
<BookingPageTagManager eventType={eventType} />
|
<BookingPageTagManager eventType={eventType} />
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||||
|
|||||||
@@ -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 { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { useForm, useFormContext } from "react-hook-form";
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -15,7 +15,6 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
|||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -36,19 +35,19 @@ import classNames from "@calcom/lib/classNames";
|
|||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
import { Button, Form, Tooltip } from "@calcom/ui";
|
||||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseRecurringDates, parseDate } from "@lib/parseDate";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import Gates from "@components/Gates";
|
import Gates from "@components/Gates";
|
||||||
@@ -58,6 +57,8 @@ import type { BookPageProps } from "../../../pages/[user]/book";
|
|||||||
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||||
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
|
|
||||||
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
|
|
||||||
/** These are like 40kb that not every user needs */
|
/** These are like 40kb that not every user needs */
|
||||||
const BookingDescriptionPayment = dynamic(
|
const BookingDescriptionPayment = dynamic(
|
||||||
() => import("@components/booking/BookingDescriptionPayment")
|
() => import("@components/booking/BookingDescriptionPayment")
|
||||||
@@ -78,6 +79,7 @@ const BookingFields = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { watch, setValue } = useFormContext();
|
const { watch, setValue } = useFormContext();
|
||||||
const locationResponse = watch("responses.location");
|
const locationResponse = watch("responses.location");
|
||||||
|
const currentView = rescheduleUid ? "reschedule" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
||||||
@@ -87,12 +89,16 @@ const BookingFields = ({
|
|||||||
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
||||||
let readOnly =
|
let readOnly =
|
||||||
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
||||||
|
|
||||||
let noLabel = false;
|
let noLabel = false;
|
||||||
let hidden = !!field.hidden;
|
let hidden = !!field.hidden;
|
||||||
|
const fieldViews = field.views;
|
||||||
|
|
||||||
|
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.name === SystemField.Enum.rescheduleReason) {
|
if (field.name === SystemField.Enum.rescheduleReason) {
|
||||||
if (!rescheduleUid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
}
|
}
|
||||||
@@ -180,9 +186,26 @@ const BookingFields = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const routerQuerySchema = z
|
||||||
|
.object({
|
||||||
|
timeFormat: z.nativeEnum(TimeFormat),
|
||||||
|
rescheduleUid: z.string().optional(),
|
||||||
|
date: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((date) => {
|
||||||
|
if (date === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const BookingPage = ({
|
const BookingPage = ({
|
||||||
eventType,
|
eventType,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking,
|
||||||
profile,
|
profile,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
@@ -218,20 +241,6 @@ const BookingPage = ({
|
|||||||
duration = Number(queryDuration);
|
duration = Number(queryDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
|
|
||||||
// At initial render, we use the default time format which is 12H
|
|
||||||
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
|
|
||||||
const parseDateFunc = useCallback(
|
|
||||||
(date: string | null | Dayjs) => {
|
|
||||||
return parseDate(date, i18n, withDefaultTimeFormat);
|
|
||||||
},
|
|
||||||
[withDefaultTimeFormat]
|
|
||||||
);
|
|
||||||
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
setWithDefaultTimeFormat(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
@@ -279,6 +288,7 @@ const BookingPage = ({
|
|||||||
return router.push({
|
return router.push({
|
||||||
pathname: `/booking/${uid}`,
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
|
isSuccessBookingPage: true,
|
||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("responses.email"),
|
email: bookingForm.getValues("responses.email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
@@ -288,11 +298,17 @@ const BookingPage = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rescheduleUid = router.query.rescheduleUid as string;
|
const {
|
||||||
|
data: { timeFormat, rescheduleUid, date },
|
||||||
|
} = useTypedQuery(routerQuerySchema);
|
||||||
|
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const date = asStringOrNull(router.query.date);
|
|
||||||
const querySchema = getBookingResponsesPartialSchema({
|
const querySchema = getBookingResponsesPartialSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: {
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
|
},
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedQuery = querySchema.parse({
|
const parsedQuery = querySchema.parse({
|
||||||
@@ -372,7 +388,8 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
responses: getBookingResponsesSchema({
|
responses: getBookingResponsesSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -390,26 +407,17 @@ const BookingPage = ({
|
|||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [],
|
let recurringStrings: string[] = [],
|
||||||
recurringDates: Date[] = [];
|
recurringDates: Date[] = [];
|
||||||
const parseRecurringDatesFunc = useCallback(
|
|
||||||
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
|
|
||||||
return parseRecurringDates(
|
|
||||||
{
|
|
||||||
startDate: date,
|
|
||||||
timeZone: timeZone(),
|
|
||||||
recurringEvent: recurringEvent,
|
|
||||||
recurringCount: recurringCount,
|
|
||||||
withDefaultTimeFormat: withDefaultTimeFormat,
|
|
||||||
},
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
|
|
||||||
);
|
|
||||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||||
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
|
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||||
date,
|
{
|
||||||
eventType.recurringEvent,
|
startDate: date,
|
||||||
parseInt(recurringEventCount.toString())
|
timeZone: timeZone(),
|
||||||
|
recurringEvent: eventType.recurringEvent,
|
||||||
|
recurringCount: parseInt(recurringEventCount.toString()),
|
||||||
|
selectedTimeFormat: timeFormat,
|
||||||
|
},
|
||||||
|
i18n
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,8 +445,8 @@ const BookingPage = ({
|
|||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||||
...bookingValues,
|
...bookingValues,
|
||||||
start: dayjs(recurringDate).format(),
|
start: dayjs(recurringDate).utc().format(),
|
||||||
end: dayjs(recurringDate).add(duration, "minute").format(),
|
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
recurringEventId,
|
recurringEventId,
|
||||||
@@ -457,8 +465,8 @@ const BookingPage = ({
|
|||||||
} else {
|
} else {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...bookingValues,
|
...bookingValues,
|
||||||
start: dayjs(date).tz(timeZone()).format(),
|
start: dayjs(date).utc().format(),
|
||||||
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
|
end: dayjs(date).utc().add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
timeZone: timeZone(),
|
timeZone: timeZone(),
|
||||||
@@ -539,7 +547,7 @@ const BookingPage = ({
|
|||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
`${parseDateFunc(date)}`}
|
`${parseDate(date, i18n, timeFormat)}`}
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
!rescheduleUid &&
|
!rescheduleUid &&
|
||||||
eventType.recurringEvent?.freq &&
|
eventType.recurringEvent?.freq &&
|
||||||
@@ -569,7 +577,7 @@ const BookingPage = ({
|
|||||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDateFunc(dayjs(booking.startTime))}
|
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -577,23 +585,27 @@ const BookingPage = ({
|
|||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiUser
|
<FiUser
|
||||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={`${
|
className={`${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
} mb-2 font-medium`}>
|
} mb-2 font-medium`}>
|
||||||
{booking
|
{currentSlotBooking
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
</p>
|
</p>
|
||||||
@@ -625,6 +637,7 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
@@ -638,6 +651,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</Gates>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Input } from "@calcom/ui";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ const LocationInput = (props: {
|
|||||||
const { control } = useFormContext() as typeof locationFormMethods;
|
const { control } = useFormContext() as typeof locationFormMethods;
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
if (eventLocationType?.organizerInputType === "text") {
|
||||||
return (
|
return (
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||||
);
|
);
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||||
return (
|
return (
|
||||||
@@ -171,7 +172,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
id="locationInput"
|
id="locationInput"
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
defaultValue={
|
defaultValue={
|
||||||
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import type { OptionProps, SingleValueProps } from "react-select";
|
|||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|||||||
|
|
||||||
const AvailabilitySelect = ({
|
const AvailabilitySelect = ({
|
||||||
className = "",
|
className = "",
|
||||||
|
isLoading,
|
||||||
|
schedules,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||||
onBlur: () => void;
|
onBlur: () => void;
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
onChange: (value: AvailabilityOption | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <SelectSkeletonLoader />;
|
return <SelectSkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedules = data?.schedules || [];
|
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
const options = schedules.map((schedule) => ({
|
||||||
value: schedule.id,
|
value: schedule.id,
|
||||||
label: schedule.name,
|
label: schedule.name,
|
||||||
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
|
|||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
|
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||||
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
|
|||||||
|
|
||||||
const EventTypeSchedule = () => {
|
const EventTypeSchedule = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||||
|
|
||||||
|
if (!schedules?.schedules.length && !isLoading)
|
||||||
|
return (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={FiClock}
|
||||||
|
headline={t("new_schedule_heading")}
|
||||||
|
description={t("new_schedule_description")}
|
||||||
|
buttonRaw={<NewScheduleButton fromEventType />}
|
||||||
|
border={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="schedule"
|
name="schedule"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<AvailabilitySelect
|
<>
|
||||||
value={field.value}
|
<AvailabilitySelect
|
||||||
onBlur={field.onBlur}
|
value={field.value}
|
||||||
name={field.name}
|
onBlur={field.onBlur}
|
||||||
onChange={(selected) => {
|
name={field.name}
|
||||||
field.onChange(selected?.value || null);
|
schedules={schedules?.schedules || []}
|
||||||
}}
|
isLoading={isLoading}
|
||||||
/>
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
label={t("minimum_booking_notice")}
|
label={t("minimum_booking_notice")}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
min={0}
|
||||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
/>
|
/>
|
||||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||||
@@ -424,7 +425,7 @@ const IntervalLimitItem = ({
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
|
|||||||
@@ -198,8 +198,16 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
locationFormMethods.setValue("locationType", location.type);
|
||||||
|
locationFormMethods.unregister("locationLink");
|
||||||
|
locationFormMethods.unregister("locationAddress");
|
||||||
|
locationFormMethods.unregister("locationPhoneNumber");
|
||||||
|
setEditingLocationType(location.type);
|
||||||
|
openLocationModal(location.type);
|
||||||
|
}}
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ function EventTypeSingleLayout({
|
|||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="destructive"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiTrash}
|
StartIcon={FiTrash}
|
||||||
tooltip={t("delete")}
|
tooltip={t("delete")}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -22,23 +25,30 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const userSettingsSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||||
|
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm<z.infer<typeof userSettingsSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: user?.name || "",
|
name: user?.name || "",
|
||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(userSettingsSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
telemetry.event(telemetryEventTypes.onboardingStarted);
|
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
const defaultOptions = { required: true, maxLength: 255 };
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const onSuccess = async () => {
|
const onSuccess = async () => {
|
||||||
await utils.viewer.me.invalidate();
|
await utils.viewer.me.invalidate();
|
||||||
@@ -67,7 +77,9 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
{t("full_name")}
|
{t("full_name")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
{...register("name", defaultOptions)}
|
{...register("name", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -77,7 +89,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||||
{t("required")}
|
{errors.name.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{props.defaultSelected && (
|
{props.defaultSelected && (
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
<div className="flex">
|
||||||
|
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { components } from "react-select";
|
|||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { Select } from "@calcom/ui";
|
||||||
import Select from "@components/ui/form/Select";
|
|
||||||
|
|
||||||
export type LocationOption = {
|
export type LocationOption = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,21 +16,11 @@ export type SingleValueLocationOption = SingleValue<LocationOption>;
|
|||||||
|
|
||||||
export type GroupOptionType = GroupBase<LocationOption>;
|
export type GroupOptionType = GroupBase<LocationOption>;
|
||||||
|
|
||||||
const OptionWithIcon = ({
|
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
||||||
icon,
|
|
||||||
isSelected,
|
|
||||||
label,
|
|
||||||
}: {
|
|
||||||
icon?: string;
|
|
||||||
isSelected?: boolean;
|
|
||||||
label: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
|
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
|
||||||
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
|
<span className={classNames("text-sm font-medium")}>{label}</span>
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -44,7 +33,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
|||||||
components={{
|
components={{
|
||||||
Option: (props) => (
|
Option: (props) => (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
|
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||||
</components.Option>
|
</components.Option>
|
||||||
),
|
),
|
||||||
SingleValue: (props) => (
|
SingleValue: (props) => (
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type { ComponentProps, ReactNode } from "react";
|
|||||||
|
|
||||||
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 { useFlags } from "@calcom/features/flags/hooks";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { MetaProvider } from "@calcom/ui";
|
import { MetaProvider } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -58,6 +60,11 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||||||
return <I18nextAdapter {...passedProps} />;
|
return <I18nextAdapter {...passedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const flags = useFlags();
|
||||||
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -85,7 +92,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
forcedTheme={forcedTheme}
|
forcedTheme={forcedTheme}
|
||||||
attribute="class">
|
attribute="class">
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<FeatureFlagsProvider>
|
||||||
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
|
</FeatureFlagsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</CustomI18nextProvider>
|
</CustomI18nextProvider>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
|
||||||
|
|
||||||
type BookingSelect = {
|
type BookingSelect = {
|
||||||
description: true;
|
description: true;
|
||||||
@@ -45,11 +44,7 @@ function getResponsesFromOldBooking(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBooking(
|
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||||
prisma: PrismaClient,
|
|
||||||
uid: string,
|
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
|
|
||||||
) {
|
|
||||||
const rawBooking = await prisma.booking.findFirst({
|
const rawBooking = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uid,
|
uid,
|
||||||
@@ -82,9 +77,7 @@ async function getBooking(
|
|||||||
return rawBooking;
|
return rawBooking;
|
||||||
}
|
}
|
||||||
|
|
||||||
const booking = getBookingWithResponses(rawBooking, {
|
const booking = getBookingWithResponses(rawBooking);
|
||||||
bookingFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (booking) {
|
if (booking) {
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
@@ -104,16 +97,11 @@ export const getBookingWithResponses = <
|
|||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
>(
|
>(
|
||||||
booking: T,
|
booking: T
|
||||||
eventType: {
|
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
|
||||||
}
|
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...booking,
|
...booking,
|
||||||
responses: getBookingResponsesPartialSchema({
|
responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||||
bookingFields: eventType.bookingFields,
|
} as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
|
||||||
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
export default getBooking;
|
export default getBooking;
|
||||||
@@ -3,23 +3,22 @@ import { RRule } from "rrule";
|
|||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { parseZone } from "./parseZone";
|
import { parseZone } from "./parseZone";
|
||||||
|
|
||||||
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
const processDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
|
||||||
const parsedZone = parseZone(date);
|
const parsedZone = parseZone(date);
|
||||||
if (!parsedZone?.isValid()) return "Invalid date";
|
if (!parsedZone?.isValid()) return "Invalid date";
|
||||||
const formattedTime = parsedZone?.format(
|
const formattedTime = parsedZone?.format(selectedTimeFormat || detectBrowserTimeFormat);
|
||||||
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
|
|
||||||
);
|
|
||||||
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
export const parseDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
|
||||||
if (!date) return ["No date"];
|
if (!date) return ["No date"];
|
||||||
return processDate(date, i18n, withDefaultTimeFormat);
|
return processDate(date, i18n, selectedTimeFormat);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseRecurringDates = (
|
export const parseRecurringDates = (
|
||||||
@@ -28,13 +27,13 @@ export const parseRecurringDates = (
|
|||||||
timeZone,
|
timeZone,
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
recurringCount,
|
recurringCount,
|
||||||
withDefaultTimeFormat,
|
selectedTimeFormat,
|
||||||
}: {
|
}: {
|
||||||
startDate: string | null | Dayjs;
|
startDate: string | null | Dayjs;
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
recurringEvent: RecurringEvent | null;
|
recurringEvent: RecurringEvent | null;
|
||||||
recurringCount: number;
|
recurringCount: number;
|
||||||
withDefaultTimeFormat: boolean;
|
selectedTimeFormat?: TimeFormat;
|
||||||
},
|
},
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
): [string[], Date[]] => {
|
): [string[], Date[]] => {
|
||||||
@@ -54,7 +53,7 @@ export const parseRecurringDates = (
|
|||||||
});
|
});
|
||||||
const dateStrings = times.map((t) => {
|
const dateStrings = times.map((t) => {
|
||||||
// finally; show in local timeZone again
|
// finally; show in local timeZone again
|
||||||
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
|
return processDate(t.tz(timeZone), i18n, selectedTimeFormat);
|
||||||
});
|
});
|
||||||
|
|
||||||
return [dateStrings, times.map((t) => t.toDate())];
|
return [dateStrings, times.map((t) => t.toDate())];
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ const nextConfig = {
|
|||||||
skipDefaultConversion: true,
|
skipDefaultConversion: true,
|
||||||
preventFullImport: true,
|
preventFullImport: true,
|
||||||
},
|
},
|
||||||
|
"@calcom/features/insights/components": {
|
||||||
|
transform: "@calcom/features/insights/components/{{member}}",
|
||||||
|
skipDefaultConversion: true,
|
||||||
|
preventFullImport: true,
|
||||||
|
},
|
||||||
|
lodash: {
|
||||||
|
transform: "lodash/{{member}}",
|
||||||
|
},
|
||||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||||
// "@calcom/ui": {
|
// "@calcom/ui": {
|
||||||
// transform: "@calcom/ui/components/{{member}}",
|
// transform: "@calcom/ui/components/{{member}}",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.7.0",
|
"version": "2.7.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
|
"@tremor/react": "^2.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@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",
|
||||||
@@ -111,8 +112,8 @@
|
|||||||
"react-multi-email": "^0.5.3",
|
"react-multi-email": "^0.5.3",
|
||||||
"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.4.0",
|
"react-select": "^5.7.0",
|
||||||
"react-timezone-select": "^1.3.2",
|
"react-timezone-select": "^1.4.0",
|
||||||
"react-use-intercom": "1.5.1",
|
"react-use-intercom": "1.5.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
|
|||||||
@@ -217,7 +217,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
rescheduleUid = bookingSeat.booking.uid;
|
rescheduleUid = bookingSeat.booking.uid;
|
||||||
attendeeEmail = bookingSeat.attendee.email;
|
attendeeEmail = bookingSeat.attendee.email;
|
||||||
}
|
}
|
||||||
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) {
|
}
|
||||||
|
|
||||||
|
if (query.duration) {
|
||||||
// If it's not reschedule but event Type has seats we should obtain
|
// If it's not reschedule but event Type has seats we should obtain
|
||||||
// the bookingUid regardless and use it to get the booking
|
// the bookingUid regardless and use it to get the booking
|
||||||
const currentSeats = await prisma.booking.findFirst({
|
const currentSeats = await prisma.booking.findFirst({
|
||||||
@@ -237,11 +239,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||||
booking = await getBooking(
|
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
|
||||||
prisma,
|
|
||||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
|
||||||
eventTypeObject.bookingFields
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||||
@@ -285,12 +283,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
: eventType.recurringEvent.count)) ||
|
: eventType.recurringEvent.count)) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
away: user.away,
|
away: user.away,
|
||||||
profile,
|
profile,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking: currentSlotBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
import type { ResetPasswordRequest } from "@prisma/client";
|
import type { ResetPasswordRequest } from "@prisma/client";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
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 { 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({
|
||||||
|
intervalInMs: 60 * 1000, // 1 minute
|
||||||
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.transform((val) => val.toLowerCase())
|
||||||
|
.parse(req.body?.email);
|
||||||
|
|
||||||
|
const { isRateLimited } = limiter.check(10, email); // 10 requests per minute
|
||||||
|
|
||||||
|
if (isRateLimited) {
|
||||||
|
return res.status(429).json({ message: "Too Many Requests." });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return res.status(405).end();
|
return res.status(405).end();
|
||||||
@@ -17,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
try {
|
try {
|
||||||
const maybeUser = await prisma.user.findUnique({
|
const maybeUser = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email: req.body?.email?.toLowerCase(),
|
email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ImageResponse } from "@vercel/og";
|
import { ImageResponse } from "@vercel/og";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import { remark } from "remark";
|
|
||||||
import type { SatoriOptions } from "satori";
|
import type { SatoriOptions } from "satori";
|
||||||
import strip from "strip-markdown";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
|
||||||
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
||||||
res.arrayBuffer()
|
res.arrayBuffer()
|
||||||
@@ -75,10 +74,13 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
meetingImage: searchParams.get("meetingImage"),
|
meetingImage: searchParams.get("meetingImage"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
|
||||||
|
|
||||||
const img = new ImageResponse(
|
const img = new ImageResponse(
|
||||||
(
|
(
|
||||||
<Meeting
|
<Meeting
|
||||||
title={title}
|
title={title_}
|
||||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||||
/>
|
/>
|
||||||
@@ -108,8 +110,8 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
description: searchParams.get("description"),
|
description: searchParams.get("description"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
const description_ = await (await remark().use(strip).process(description)).toString();
|
|
||||||
const img = new ImageResponse(<Generic title={title} description={description_} />, ogConfig) as {
|
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import matter from "gray-matter";
|
|||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -13,6 +14,23 @@ import App from "@components/apps/App";
|
|||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||||
|
|
||||||
|
const sourceSchema = z.object({
|
||||||
|
content: z.string(),
|
||||||
|
data: z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
items: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
iframe: z.object({ src: z.string() }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<App
|
<App
|
||||||
@@ -33,7 +51,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
email={data.email}
|
email={data.email}
|
||||||
licenseRequired={data.licenseRequired}
|
licenseRequired={data.licenseRequired}
|
||||||
isProOnly={data.isProOnly}
|
isProOnly={data.isProOnly}
|
||||||
images={source.data?.items as string[] | undefined}
|
descriptionItems={source.data?.items as string[] | undefined}
|
||||||
isTemplate={data.isTemplate}
|
isTemplate={data.isTemplate}
|
||||||
dependencies={data.dependencies}
|
dependencies={data.dependencies}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
@@ -85,10 +103,11 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
source = singleApp.description;
|
source = singleApp.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { content, data } = matter(source);
|
const result = matter(source);
|
||||||
|
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||||
if (data.items) {
|
if (data.items) {
|
||||||
data.items = data.items.map((item: string) => {
|
data.items = data.items.map((item) => {
|
||||||
if (!item.includes("/api/app-store")) {
|
if (typeof item === "string" && !item.includes("/api/app-store")) {
|
||||||
// Make relative paths absolute
|
// Make relative paths absolute
|
||||||
return `/api/app-store/${appDirname}/${item}`;
|
return `/api/app-store/${appDirname}/${item}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ResetPasswordRequest } from "@prisma/client";
|
import type { ResetPasswordRequest } from "@prisma/client";
|
||||||
import debounce from "lodash/debounce";
|
import { debounce } from "lodash";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken } from "next-auth/react";
|
import { getCsrfToken } from "next-auth/react";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import debounce from "lodash/debounce";
|
import { debounce } from "lodash";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken } from "next-auth/react";
|
import { getCsrfToken } from "next-auth/react";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function Logout(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button href="/auth/login" passHref className="flex w-full justify-center">
|
<Button href="/auth/login" className="flex w-full justify-center">
|
||||||
{t("go_back_login")}
|
{t("go_back_login")}
|
||||||
</Button>
|
</Button>
|
||||||
</AuthContainer>
|
</AuthContainer>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export default function Availability() {
|
|||||||
data: { schedule: scheduleId },
|
data: { schedule: scheduleId },
|
||||||
} = useTypedQuery(querySchema);
|
} = useTypedQuery(querySchema);
|
||||||
|
|
||||||
|
const { fromEventType } = router.query;
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId },
|
{ scheduleId },
|
||||||
@@ -153,7 +154,7 @@ export default function Availability() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/availability"
|
backPath={fromEventType ? true : "/availability"}
|
||||||
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
||||||
heading={
|
heading={
|
||||||
<Controller
|
<Controller
|
||||||
@@ -200,7 +201,14 @@ export default function Availability() {
|
|||||||
<VerticalDivider />
|
<VerticalDivider />
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} />
|
<Button
|
||||||
|
StartIcon={FiTrash}
|
||||||
|
variant="icon"
|
||||||
|
color="destructive"
|
||||||
|
aria-label={t("delete")}
|
||||||
|
disabled={schedule?.isLastSchedule}
|
||||||
|
tooltip={t("requires_at_least_one_schedule")}
|
||||||
|
/>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
@@ -223,7 +231,7 @@ export default function Availability() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="w-full">
|
<div className="mt-4 w-full md:mt-0">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
id="availability-form"
|
id="availability-form"
|
||||||
|
|||||||
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
key={schedule.id}
|
key={schedule.id}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
updateDefault={updateMutation.mutate}
|
updateDefault={updateMutation.mutate}
|
||||||
deleteFunction={deleteMutation.mutate}
|
deleteFunction={() => {
|
||||||
|
if (schedules.length === 1) {
|
||||||
|
showToast(t("requires_at_least_one_schedule"), "error");
|
||||||
|
} else {
|
||||||
|
deleteMutation.mutate;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const querySchema = z.object({
|
|||||||
cancel: stringToBoolean,
|
cancel: stringToBoolean,
|
||||||
changes: stringToBoolean,
|
changes: stringToBoolean,
|
||||||
reschedule: stringToBoolean,
|
reschedule: stringToBoolean,
|
||||||
isSuccessBookingPage: z.string().optional(),
|
isSuccessBookingPage: stringToBoolean,
|
||||||
formerTime: z.string().optional(),
|
formerTime: z.string().optional(),
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
seatReferenceUid: z.string().optional(),
|
seatReferenceUid: z.string().optional(),
|
||||||
@@ -266,16 +266,18 @@ export default function Success(props: SuccessProps) {
|
|||||||
const giphyImage = giphyAppData?.thankYouPage;
|
const giphyImage = giphyAppData?.thankYouPage;
|
||||||
|
|
||||||
const eventName = getEventName(eventNameObject, true);
|
const eventName = getEventName(eventNameObject, true);
|
||||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
|
// Confirmation can be needed in two cases as of now
|
||||||
|
// - Event Type has require confirmation option enabled always
|
||||||
|
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||||
|
// - It's a paid event and payment is pending.
|
||||||
|
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||||
|
|
||||||
const isCancelled =
|
const isCancelled =
|
||||||
status === "CANCELLED" ||
|
status === "CANCELLED" ||
|
||||||
status === "REJECTED" ||
|
status === "REJECTED" ||
|
||||||
(isCancellationMode &&
|
(!!seatReferenceUid &&
|
||||||
(!!seatReferenceUid
|
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
||||||
? !bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid)
|
|
||||||
: !userIsOwner));
|
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -433,7 +435,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
: "",
|
: "",
|
||||||
isCancelled ? "h-12 w-12 rounded-full bg-red-100" : ""
|
isCancelled ? "h-12 w-12 rounded-full bg-red-100" : ""
|
||||||
)}>
|
)}>
|
||||||
{giphyImage && !needsConfirmation && (
|
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img src={giphyImage} alt="Gif from Giphy" />
|
<img src={giphyImage} alt="Gif from Giphy" />
|
||||||
)}
|
)}
|
||||||
@@ -487,7 +489,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="font-medium">{t("what")}</div>
|
<div className="font-medium">{t("what")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
<div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</div>
|
||||||
<div className="font-medium">{t("when")}</div>
|
<div className="font-medium">{t("when")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">
|
<div className="col-span-2 mb-6 last:mb-0">
|
||||||
{reschedule && !!formerTime && (
|
{reschedule && !!formerTime && (
|
||||||
@@ -529,9 +531,11 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bookingInfo?.attendees.map((attendee) => (
|
{bookingInfo?.attendees.map((attendee) => (
|
||||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||||
{attendee.name && <p>{attendee.name}</p>}
|
{attendee.name && <p>{attendee.name}</p>}
|
||||||
<p className="text-bookinglight">{attendee.email}</p>
|
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
||||||
|
{attendee.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -606,7 +610,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
{!props.recurringBookings && (
|
{!props.recurringBookings && (
|
||||||
<span className="text-bookinglight inline text-gray-700">
|
<span className="text-bookinglight inline text-gray-700">
|
||||||
<span className="underline">
|
<span className="underline" data-testid="reschedule-link">
|
||||||
<Link
|
<Link
|
||||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
||||||
legacyBehavior>
|
legacyBehavior>
|
||||||
@@ -1087,8 +1091,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||||
|
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
profile,
|
profile,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking: null,
|
booking: null,
|
||||||
|
currentSlotBooking: null,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
|
|||||||
@@ -351,11 +351,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{type.hidden && (
|
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||||
<Badge variant="gray" size="lg">
|
|
||||||
{t("hidden")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||||
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
AverageEventDurationChart,
|
||||||
|
BookingKPICards,
|
||||||
|
BookingStatusLineChart,
|
||||||
|
LeastBookedTeamMembersTable,
|
||||||
|
MostBookedTeamMembersTable,
|
||||||
|
PopularEventsTable,
|
||||||
|
} from "@calcom/features/insights/components";
|
||||||
|
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||||
|
import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||||
|
import { Filters } from "@calcom/features/insights/filters";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { UpgradeTip } from "@calcom/features/tips";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import { Button, ButtonGroup } from "@calcom/ui";
|
||||||
|
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
const Heading = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const {
|
||||||
|
filter: { selectedTeamName },
|
||||||
|
} = useFilterContext();
|
||||||
|
return (
|
||||||
|
<div className="min-w-52">
|
||||||
|
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-xl font-semibold tracking-wide text-black md:block xl:max-w-full">
|
||||||
|
{t("analytics_for_organisation", {
|
||||||
|
organisationName: selectedTeamName,
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InsightsPage() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: <FiUsers className="h-5 w-5 text-red-500" />,
|
||||||
|
title: t("view_bookings_across"),
|
||||||
|
description: t("view_bookings_across_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiRefreshCcw className="h-5 w-5 text-blue-500" />,
|
||||||
|
title: t("identify_booking_trends"),
|
||||||
|
description: t("identify_booking_trends_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiUserPlus className="h-5 w-5 text-green-500" />,
|
||||||
|
title: t("spot_popular_event_types"),
|
||||||
|
description: t("spot_popular_event_types_description"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Shell>
|
||||||
|
<UpgradeTip
|
||||||
|
title={t("make_informed_decisions")}
|
||||||
|
description={t("make_informed_decisions_description")}
|
||||||
|
features={features}
|
||||||
|
background="/banners/insights.jpg"
|
||||||
|
buttons={
|
||||||
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||||
|
{t("create_team")}
|
||||||
|
</Button>
|
||||||
|
<Button color="secondary" href="https://go.cal.com/insights" target="_blank">
|
||||||
|
{t("learn_more")}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
{!user ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<FiltersProvider>
|
||||||
|
<div className="mb-4 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
|
||||||
|
<Heading />
|
||||||
|
<Filters />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 space-y-6">
|
||||||
|
<BookingKPICards />
|
||||||
|
|
||||||
|
<BookingStatusLineChart />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||||
|
<PopularEventsTable />
|
||||||
|
|
||||||
|
<AverageEventDurationChart />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||||
|
<MostBookedTeamMembersTable />
|
||||||
|
<LeastBookedTeamMembersTable />
|
||||||
|
</div>
|
||||||
|
<small className="block text-center text-gray-600">
|
||||||
|
{t("looking_for_more_analytics")}
|
||||||
|
<a
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||||
|
{" "}
|
||||||
|
{t("contact_support")}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</FiltersProvider>
|
||||||
|
)}
|
||||||
|
</UpgradeTip>
|
||||||
|
</Shell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||||
|
|
||||||
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
|
const FlagsPage = () => <FlagListingView />;
|
||||||
|
|
||||||
|
FlagsPage.getLayout = getLayout;
|
||||||
|
|
||||||
|
export default FlagsPage;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
|
||||||
|
|
||||||
|
import { useIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
@@ -33,15 +32,13 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
|||||||
|
|
||||||
const BillingView = () => {
|
const BillingView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [, loadChat] = useChat();
|
const { open } = useIntercom();
|
||||||
const [showChat, setShowChat] = useState(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const returnTo = router.asPath;
|
const returnTo = router.asPath;
|
||||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
|
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
|
||||||
|
|
||||||
const onContactSupportClick = () => {
|
const onContactSupportClick = () => {
|
||||||
setShowChat(true);
|
open();
|
||||||
loadChat({ open: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +58,6 @@ const BillingView = () => {
|
|||||||
{t("contact_support")}
|
{t("contact_support")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import type { BaseSyntheticEvent } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
@@ -315,13 +317,23 @@ const ProfileForm = ({
|
|||||||
extraField?: React.ReactNode;
|
extraField?: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const emailMd5 = crypto
|
|
||||||
.createHash("md5")
|
const profileFormSchema = z.object({
|
||||||
.update(defaultValues.email || "example@example.com")
|
username: z.string(),
|
||||||
.digest("hex");
|
avatar: z.string(),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||||
|
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||||
|
}),
|
||||||
|
email: z.string().email(),
|
||||||
|
bio: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
|
resolver: zodResolver(profileFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -338,7 +350,7 @@ const ProfileForm = ({
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<>
|
<>
|
||||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
|
||||||
<div className="ltr:ml-4 rtl:mr-4">
|
<div className="ltr:ml-4 rtl:mr-4">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
target="avatar"
|
target="avatar"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const PasswordView = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const metadata = userMetadata.parse(user?.metadata);
|
const metadata = userMetadata.safeParse(user?.metadata);
|
||||||
|
const sessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
|
||||||
|
|
||||||
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -36,10 +37,10 @@ const PasswordView = () => {
|
|||||||
const previousValue = utils.viewer.me.getData();
|
const previousValue = utils.viewer.me.getData();
|
||||||
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
||||||
|
|
||||||
if (previousValue && metadata?.sessionTimeout) {
|
if (previousValue && sessionTimeout) {
|
||||||
utils.viewer.me.setData(undefined, {
|
utils.viewer.me.setData(undefined, {
|
||||||
...previousValue,
|
...previousValue,
|
||||||
metadata: { ...previousMetadata, sessionTimeout: metadata?.sessionTimeout },
|
metadata: { ...previousMetadata, sessionTimeout: sessionTimeout },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { previousValue };
|
return { previousValue };
|
||||||
@@ -81,19 +82,19 @@ const PasswordView = () => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
sessionTimeout: metadata?.sessionTimeout,
|
sessionTimeout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
||||||
|
|
||||||
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
||||||
const { oldPassword, newPassword, sessionTimeout } = values;
|
const { oldPassword, newPassword, sessionTimeout: newSessionTimeout } = values;
|
||||||
if (oldPassword && newPassword) {
|
if (oldPassword && newPassword) {
|
||||||
passwordMutation.mutate({ oldPassword, newPassword });
|
passwordMutation.mutate({ oldPassword, newPassword });
|
||||||
}
|
}
|
||||||
if (metadata?.sessionTimeout !== sessionTimeout) {
|
if (sessionTimeout !== newSessionTimeout) {
|
||||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout } });
|
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout: newSessionTimeout } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ const PasswordView = () => {
|
|||||||
|
|
||||||
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
|
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
|
||||||
const isUser = data?.user.role === "USER";
|
const isUser = data?.user.role === "USER";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={t("password")} description={t("password_description")} />
|
<Meta title={t("password")} description={t("password_description")} />
|
||||||
@@ -176,8 +178,8 @@ const PasswordView = () => {
|
|||||||
<Select
|
<Select
|
||||||
options={timeoutOptions}
|
options={timeoutOptions}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
metadata?.sessionTimeout
|
sessionTimeout
|
||||||
? timeoutOptions.find((tmo) => tmo.value === metadata.sessionTimeout)
|
? timeoutOptions.find((tmo) => tmo.value === sessionTimeout)
|
||||||
: timeoutOptions[1]
|
: timeoutOptions[1]
|
||||||
}
|
}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -177,7 +176,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
booking = await getBooking(prisma, rescheduleUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||||
if (rescheduleUid || bookingUid) {
|
if (rescheduleUid || bookingUid) {
|
||||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields);
|
booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||||
@@ -152,6 +152,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking: null,
|
||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
hasHashedBookingLink: false,
|
hasHashedBookingLink: false,
|
||||||
hashedLink: null,
|
hashedLink: null,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ interface ProgressBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProgressBar(props: ProgressBarProps) {
|
function ProgressBar(props: ProgressBarProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
const { startTime, endTime } = props;
|
const { startTime, endTime } = props;
|
||||||
const currentTime = dayjs().second(0).millisecond(0);
|
const currentTime = dayjs().second(0).millisecond(0);
|
||||||
const startingTime = dayjs(startTime).second(0).millisecond(0);
|
const startingTime = dayjs(startTime).second(0).millisecond(0);
|
||||||
@@ -111,7 +112,9 @@ function ProgressBar(props: ProgressBarProps) {
|
|||||||
const percentage = prev * (100 / startDuration);
|
const percentage = prev * (100 / startDuration);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{duration} minutes</p>
|
<p>
|
||||||
|
{duration} {t("minutes")}
|
||||||
|
</p>
|
||||||
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
|
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
|
||||||
<div className="absolute h-full w-full bg-gray-500/10" />
|
<div className="absolute h-full w-full bg-gray-500/10" />
|
||||||
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
|
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
|
||||||
@@ -127,6 +130,7 @@ interface VideoMeetingInfo {
|
|||||||
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { booking } = props;
|
const { booking } = props;
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
const endTime = new Date(booking.endTime);
|
const endTime = new Date(booking.endTime);
|
||||||
const startTime = new Date(booking.startTime);
|
const startTime = new Date(booking.startTime);
|
||||||
@@ -139,25 +143,25 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
|||||||
open ? "translate-x-0" : "-translate-x-[232px]"
|
open ? "translate-x-0" : "-translate-x-[232px]"
|
||||||
)}>
|
)}>
|
||||||
<main className="prose-sm prose max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
|
<main className="prose-sm prose max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
|
||||||
<h3>What:</h3>
|
<h3>{t("what")}:</h3>
|
||||||
<p>{booking.title}</p>
|
<p>{booking.title}</p>
|
||||||
<h3>Invitee Time Zone:</h3>
|
<h3>{t("invitee_timezone")}:</h3>
|
||||||
<p>{booking.user?.timeZone}</p>
|
<p>{booking.user?.timeZone}</p>
|
||||||
<h3>When:</h3>
|
<h3>{t("when")}:</h3>
|
||||||
<p>
|
<p>
|
||||||
{formatToLocalizedDate(startTime)} <br />
|
{formatToLocalizedDate(startTime)} <br />
|
||||||
{formatToLocalizedTime(startTime)}
|
{formatToLocalizedTime(startTime)}
|
||||||
</p>
|
</p>
|
||||||
<h3>Time left</h3>
|
<h3>{t("time_left")}</h3>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
key={String(open)}
|
key={String(open)}
|
||||||
endTime={endTime.toISOString()}
|
endTime={endTime.toISOString()}
|
||||||
startTime={startTime.toISOString()}
|
startTime={startTime.toISOString()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3>Who:</h3>
|
<h3>{t("who")}:</h3>
|
||||||
<p>
|
<p>
|
||||||
{booking?.user?.name} - Organizer{" "}
|
{booking?.user?.name} - {t("organizer")}:{" "}
|
||||||
<a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
|
<a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -169,12 +173,16 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
|||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<h3>Description</h3>
|
{booking.description && (
|
||||||
|
<>
|
||||||
|
<h3>{t("description")}:</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="prose-sm prose prose-invert"
|
className="prose-sm prose prose-invert"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(booking.description ?? "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(booking.description ?? "") }}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -132,4 +132,26 @@ test.describe("pro user", () => {
|
|||||||
// This is the only booking in there that needed confirmation and now it should be empty screen
|
// This is the only booking in there that needed confirmation and now it should be empty screen
|
||||||
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can book with multiple guests", async ({ page, users }) => {
|
||||||
|
const additionalGuests = ["test@gmail.com", "test2@gmail.com"];
|
||||||
|
|
||||||
|
await page.click('[data-testid="event-type-link"]');
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await page.fill('[name="name"]', "test1234");
|
||||||
|
await page.fill('[name="email"]', "test1234@example.com");
|
||||||
|
await page.locator('[data-testid="add-guests"]').click();
|
||||||
|
|
||||||
|
await page.locator('input[type="email"]').nth(1).fill(additionalGuests[0]);
|
||||||
|
await page.locator('[data-testid="add-another-guest"]').click();
|
||||||
|
await page.locator('input[type="email"]').nth(2).fill(additionalGuests[1]);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="confirm-book-button"]').click();
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
additionalGuests.forEach(async (email) => {
|
||||||
|
await expect(page.locator(`[data-testid="attendee-${email}"]`)).toHaveText(email);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -119,7 +119,7 @@ test.describe("Event Types tests", () => {
|
|||||||
await page.locator("#location-select").click();
|
await page.locator("#location-select").click();
|
||||||
await page.locator("text=In Person (Organizer Address)").click();
|
await page.locator("text=In Person (Organizer Address)").click();
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
await page.waitForTimeout(250);
|
await page.waitForTimeout(1000);
|
||||||
await page.locator('input[name="locationAddress"]').fill(inputText);
|
await page.locator('input[name="locationAddress"]').fill(inputText);
|
||||||
await page.locator("[data-testid=display-location]").check();
|
await page.locator("[data-testid=display-location]").check();
|
||||||
await page.locator("[data-testid=update-location]").click();
|
await page.locator("[data-testid=update-location]").click();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
|
|||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
import type { IncomingMessage, ServerResponse } from "http";
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import noop from "lodash/noop";
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import { test } from "./fixtures";
|
import { test } from "./fixtures";
|
||||||
|
|
||||||
|
|||||||
@@ -33,56 +33,108 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
await firstEventTypeElement.click();
|
await firstEventTypeElement.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||||
await addQuestionAndSave({
|
});
|
||||||
page,
|
});
|
||||||
question: {
|
|
||||||
name: "how_are_you",
|
|
||||||
type: "Name",
|
|
||||||
label: "How are you?",
|
|
||||||
placeholder: "I'm fine, thanks",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
test.describe("For Team EventType", () => {
|
||||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
||||||
const userFieldLocator = allFieldsLocator.nth(5);
|
page,
|
||||||
|
users,
|
||||||
|
context,
|
||||||
|
}, testInfo) => {
|
||||||
|
// Considering there are many steps in it, it would need more than default test timeout
|
||||||
|
test.setTimeout(testInfo.timeout * 3);
|
||||||
|
const user = await createAndLoginUserWithEventTypes({ users });
|
||||||
|
|
||||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
const webhookReceiver = await addWebhook(user);
|
||||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
|
||||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
await test.step("Go to First Team Event", async () => {
|
||||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
||||||
});
|
const firstEventTypeElement = $eventTypes.first();
|
||||||
|
|
||||||
|
await firstEventTypeElement.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||||
await toggleQuestionAndSave({
|
});
|
||||||
name: "how_are_you",
|
});
|
||||||
page,
|
});
|
||||||
});
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Show Question Again", async () => {
|
async function runTestStepsCommonForTeamAndUserEventType(
|
||||||
await toggleQuestionAndSave({
|
page: Page,
|
||||||
name: "how_are_you",
|
context: PlaywrightTestArgs["context"],
|
||||||
page,
|
webhookReceiver: {
|
||||||
});
|
port: number;
|
||||||
});
|
close: () => import("http").Server;
|
||||||
|
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
await page.click('[href$="tabName=advanced"]');
|
||||||
|
|
||||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
await addQuestionAndSave({
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
page,
|
||||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
question: {
|
||||||
});
|
name: "how_are_you",
|
||||||
});
|
type: "Name",
|
||||||
|
label: "How are you?",
|
||||||
|
placeholder: "I'm fine, thanks",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await test.step("Do a booking", async () => {
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||||
|
const userFieldLocator = allFieldsLocator.nth(5);
|
||||||
|
|
||||||
|
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||||
|
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||||
|
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||||
|
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||||
|
await toggleQuestionAndSave({
|
||||||
|
name: "how_are_you",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
|
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||||
|
await expect(formBuilderFieldLocator).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Show Question Again", async () => {
|
||||||
|
await toggleQuestionAndSave({
|
||||||
|
name: "how_are_you",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||||
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
|
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||||
|
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Make rescheduleReason required - It won't be required for a fresh booking", async () => {
|
||||||
|
await toggleQuestionRequireStatusAndSave({
|
||||||
|
required: true,
|
||||||
|
name: "rescheduleReason",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewTabPage =
|
||||||
|
await test.step("Do a booking and notice that we can book without giving a value for rescheduleReason", async () => {
|
||||||
|
return await doOnFreshPreview(
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
async (page) => {
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||||
await expect(formBuilderFieldLocator).toBeVisible();
|
await expect(formBuilderFieldLocator).toBeVisible();
|
||||||
expect(
|
expect(
|
||||||
@@ -133,146 +185,16 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
value: "I am great!",
|
value: "I am great!",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
true
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("For Team EventType", () => {
|
|
||||||
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
|
||||||
page,
|
|
||||||
users,
|
|
||||||
context,
|
|
||||||
}, testInfo) => {
|
|
||||||
// Considering there are many steps in it, it would need more than default test timeout
|
|
||||||
test.setTimeout(testInfo.timeout * 3);
|
|
||||||
const user = await createAndLoginUserWithEventTypes({ users });
|
|
||||||
|
|
||||||
const webhookReceiver = await addWebhook(user);
|
|
||||||
|
|
||||||
await test.step("Go to First Team Event", async () => {
|
|
||||||
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
|
||||||
const firstEventTypeElement = $eventTypes.first();
|
|
||||||
|
|
||||||
await firstEventTypeElement.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function runTestStepsCommonForTeamAndUserEventType(
|
|
||||||
page: Page,
|
|
||||||
context: PlaywrightTestArgs["context"],
|
|
||||||
webhookReceiver: {
|
|
||||||
port: number;
|
|
||||||
close: () => import("http").Server;
|
|
||||||
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
|
||||||
await addQuestionAndSave({
|
|
||||||
page,
|
|
||||||
question: {
|
|
||||||
name: "how_are_you",
|
|
||||||
type: "Name",
|
|
||||||
label: "How are you?",
|
|
||||||
placeholder: "I'm fine, thanks",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
|
||||||
const userFieldLocator = allFieldsLocator.nth(5);
|
|
||||||
|
|
||||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
|
||||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
|
||||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
|
||||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
|
||||||
await toggleQuestionAndSave({
|
|
||||||
name: "how_are_you",
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Show Question Again", async () => {
|
|
||||||
await toggleQuestionAndSave({
|
|
||||||
name: "how_are_you",
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
|
||||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Do a booking", async () => {
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeVisible();
|
|
||||||
expect(await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")).toBe(
|
|
||||||
"I'm fine, thanks"
|
|
||||||
);
|
);
|
||||||
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
|
||||||
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
|
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
|
|
||||||
).toBe("I am great!");
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(webhookReceiver.requestList.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [request] = webhookReceiver.requestList;
|
|
||||||
|
|
||||||
const payload = (request.body as any).payload as any;
|
|
||||||
|
|
||||||
expect(payload.responses).toMatchObject({
|
|
||||||
email: {
|
|
||||||
label: "email_address",
|
|
||||||
value: "booker@example.com",
|
|
||||||
},
|
|
||||||
how_are_you: {
|
|
||||||
label: "How are you?",
|
|
||||||
value: "I am great!",
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
label: "your_name",
|
|
||||||
value: "Booker",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload.location).toBe("integrations:daily");
|
|
||||||
|
|
||||||
expect(payload.attendees[0]).toMatchObject({
|
|
||||||
name: "Booker",
|
|
||||||
email: "booker@example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload.userFieldsResponses).toMatchObject({
|
|
||||||
how_are_you: {
|
|
||||||
label: "How are you?",
|
|
||||||
value: "I am great!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => {
|
||||||
|
const page = previewTabPage;
|
||||||
|
await rescheduleFromTheLinkOnPage({ page });
|
||||||
|
await page.pause();
|
||||||
|
await expectErrorToBeThereFor({ page, name: "rescheduleReason" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +255,6 @@ async function addQuestionAndSave({
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
await page.click('[href$="tabName=advanced"]');
|
|
||||||
await page.click('[data-testid="add-field"]');
|
await page.click('[data-testid="add-field"]');
|
||||||
|
|
||||||
if (question.type !== undefined) {
|
if (question.type !== undefined) {
|
||||||
@@ -379,11 +300,15 @@ async function expectErrorToBeThereFor({ page, name }: { page: Page; name: strin
|
|||||||
async function doOnFreshPreview(
|
async function doOnFreshPreview(
|
||||||
page: Page,
|
page: Page,
|
||||||
context: PlaywrightTestArgs["context"],
|
context: PlaywrightTestArgs["context"],
|
||||||
callback: (page: Page) => Promise<void>
|
callback: (page: Page) => Promise<void>,
|
||||||
|
persistTab = false
|
||||||
) {
|
) {
|
||||||
const previewTabPage = await openBookingFormInPreviewTab(context, page);
|
const previewTabPage = await openBookingFormInPreviewTab(context, page);
|
||||||
await callback(previewTabPage);
|
await callback(previewTabPage);
|
||||||
await previewTabPage.close();
|
if (!persistTab) {
|
||||||
|
await previewTabPage.close();
|
||||||
|
}
|
||||||
|
return previewTabPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
||||||
@@ -391,6 +316,25 @@ async function toggleQuestionAndSave({ name, page }: { name: string; page: Page
|
|||||||
await saveEventType(page);
|
await saveEventType(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleQuestionRequireStatusAndSave({
|
||||||
|
required,
|
||||||
|
name,
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
required: boolean;
|
||||||
|
name: string;
|
||||||
|
page: Page;
|
||||||
|
}) {
|
||||||
|
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="edit-field-dialog"]')
|
||||||
|
.locator('[data-testid="field-required"] button')
|
||||||
|
.locator(`text=${required ? "Yes" : "No"}`)
|
||||||
|
.click();
|
||||||
|
await page.locator('[data-testid="field-add-save"]').click();
|
||||||
|
await saveEventType(page);
|
||||||
|
}
|
||||||
|
|
||||||
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
||||||
const user = await users.create(null, {
|
const user = await users.create(null, {
|
||||||
hasTeam: true,
|
hasTeam: true,
|
||||||
@@ -399,6 +343,16 @@ async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<t
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rescheduleFromTheLinkOnPage({ page }: { page: Page }) {
|
||||||
|
await page.locator('[data-testid="reschedule-link"]').click();
|
||||||
|
await page.waitForLoadState();
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await page.waitForNavigation({
|
||||||
|
url: (url) => url.pathname.endsWith("/book"),
|
||||||
|
});
|
||||||
|
await page.click('[data-testid="confirm-reschedule-button"]');
|
||||||
|
}
|
||||||
|
|
||||||
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
|
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
|
||||||
const previewTabPromise = context.waitForEvent("page");
|
const previewTabPromise = context.waitForEvent("page");
|
||||||
await page.locator('[data-testid="preview-button"]').click();
|
await page.locator('[data-testid="preview-button"]').click();
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ test.describe("Onboarding", () => {
|
|||||||
await page.locator("input[name=username]").fill("new user onboarding");
|
await page.locator("input[name=username]").fill("new user onboarding");
|
||||||
await page.locator("input[name=name]").fill("new user 2");
|
await page.locator("input[name=name]").fill("new user 2");
|
||||||
await page.locator("input[role=combobox]").click();
|
await page.locator("input[role=combobox]").click();
|
||||||
await page.locator("text=Europe/London GMT +0:00").click();
|
await page
|
||||||
|
.locator("*")
|
||||||
|
.filter({ hasText: /^Europe\/London/ })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
await page.locator("button[type=submit]").click();
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
|||||||
@@ -97,11 +97,12 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
|
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
// NOTE: remove if old booking should not be deleted
|
|
||||||
expect(await (await booking.self()).status).toBe(BookingStatus.CANCELLED);
|
|
||||||
|
|
||||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
||||||
|
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } });
|
||||||
|
|
||||||
expect(newBooking).not.toBeNull();
|
expect(newBooking).not.toBeNull();
|
||||||
|
expect(rescheduledBooking).toBeNull();
|
||||||
|
|
||||||
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,7 +149,6 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*payment/);
|
await expect(page).toHaveURL(/.*payment/);
|
||||||
await payment.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||||
@@ -172,8 +172,6 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*booking/);
|
await expect(page).toHaveURL(/.*booking/);
|
||||||
|
|
||||||
await payment.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 56 KiB |
@@ -90,6 +90,7 @@
|
|||||||
"new_event_scheduled_recurring": "A new recurring event has been scheduled.",
|
"new_event_scheduled_recurring": "A new recurring event has been scheduled.",
|
||||||
"invitee_email": "Invitee Email",
|
"invitee_email": "Invitee Email",
|
||||||
"invitee_timezone": "Invitee Time Zone",
|
"invitee_timezone": "Invitee Time Zone",
|
||||||
|
"time_left": "Time left",
|
||||||
"event_type": "Event Type",
|
"event_type": "Event Type",
|
||||||
"enter_meeting": "Enter Meeting",
|
"enter_meeting": "Enter Meeting",
|
||||||
"video_call_provider": "Video call provider",
|
"video_call_provider": "Video call provider",
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
||||||
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
||||||
"upgrade_to_per_seat": "Upgrade to Per-Seat",
|
"upgrade_to_per_seat": "Upgrade to Per-Seat",
|
||||||
"seat_options_doesnt_support_confirmation":"Seats option doesn't support confirmation requirement",
|
"seat_options_doesnt_support_confirmation": "Seats option doesn't support confirmation requirement",
|
||||||
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
|
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
|
||||||
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
|
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
|
||||||
"team_upgrade_banner_action": "Upgrade here",
|
"team_upgrade_banner_action": "Upgrade here",
|
||||||
@@ -432,6 +433,7 @@
|
|||||||
"password_hint_min": "Minimum 7 characters long",
|
"password_hint_min": "Minimum 7 characters long",
|
||||||
"password_hint_admin_min": "Minimum 15 characters long",
|
"password_hint_admin_min": "Minimum 15 characters long",
|
||||||
"password_hint_num": "Contain at least 1 number",
|
"password_hint_num": "Contain at least 1 number",
|
||||||
|
"max_limit_allowed_hint": "Must be {{limit}} or fewer characters long",
|
||||||
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
||||||
"incorrect_password": "Password is incorrect.",
|
"incorrect_password": "Password is incorrect.",
|
||||||
"incorrect_username_password": "Username or password is incorrect.",
|
"incorrect_username_password": "Username or password is incorrect.",
|
||||||
@@ -861,7 +863,7 @@
|
|||||||
"add_new_calendar": "Add a new calendar",
|
"add_new_calendar": "Add a new calendar",
|
||||||
"set_calendar": "Set where to add new events to when you're booked.",
|
"set_calendar": "Set where to add new events to when you're booked.",
|
||||||
"delete_schedule": "Delete schedule",
|
"delete_schedule": "Delete schedule",
|
||||||
"delete_schedule_description":"Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
"delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
||||||
"schedule_created_successfully": "{{scheduleName}} schedule created successfully",
|
"schedule_created_successfully": "{{scheduleName}} schedule created successfully",
|
||||||
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
||||||
"schedule_deleted_successfully": "Schedule deleted successfully",
|
"schedule_deleted_successfully": "Schedule deleted successfully",
|
||||||
@@ -1629,22 +1631,30 @@
|
|||||||
"a_routing_form": "A Routing Form",
|
"a_routing_form": "A Routing Form",
|
||||||
"form_description_placeholder": "Form Description",
|
"form_description_placeholder": "Form Description",
|
||||||
"keep_me_connected_with_form": "Keep me connected with the form",
|
"keep_me_connected_with_form": "Keep me connected with the form",
|
||||||
"fields_in_form_duplicated":"Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
"fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
||||||
"form_deleted": "Form deleted",
|
"form_deleted": "Form deleted",
|
||||||
"delete_form": "Are you sure you want to delete this form?",
|
"delete_form": "Are you sure you want to delete this form?",
|
||||||
"delete_form_action": "Yes, delete Form",
|
"delete_form_action": "Yes, delete Form",
|
||||||
"delete_form_confirmation":"Anyone who you've shared the link with will no longer be able to access it.",
|
"delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.",
|
||||||
"delete_form_confirmation_2":"All associated responses will be deleted.",
|
"delete_form_confirmation_2": "All associated responses will be deleted.",
|
||||||
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
|
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
|
||||||
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
|
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
|
||||||
"connected_forms": "Connected Forms",
|
"connected_forms": "Connected Forms",
|
||||||
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
|
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
|
||||||
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
|
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
|
||||||
"this_is_what_your_users_would_see":"This is what your users would see",
|
"this_is_what_your_users_would_see": "This is what your users would see",
|
||||||
"identifies_name_field": "Identifies field by this name.",
|
"identifies_name_field": "Identifies field by this name.",
|
||||||
"add_1_option_per_line": "Add 1 option per line",
|
"add_1_option_per_line": "Add 1 option per line",
|
||||||
"select_a_router": "Select a router",
|
"select_a_router": "Select a router",
|
||||||
"add_a_new_route": "Add a new Route",
|
"add_a_new_route": "Add a new Route",
|
||||||
|
"make_informed_decisions": "Make informed decisions with Insights",
|
||||||
|
"make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.",
|
||||||
|
"view_bookings_across": "View bookings across all members",
|
||||||
|
"view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team",
|
||||||
|
"identify_booking_trends": "Identify booking trends",
|
||||||
|
"identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers",
|
||||||
|
"spot_popular_event_types": "Spot popular event types",
|
||||||
|
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
|
||||||
"no_responses_yet": "No responses yet",
|
"no_responses_yet": "No responses yet",
|
||||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||||
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
||||||
@@ -1655,8 +1665,28 @@
|
|||||||
"verification_code": "Verification code",
|
"verification_code": "Verification code",
|
||||||
"can_you_try_again": "Can you try again with a different time?",
|
"can_you_try_again": "Can you try again with a different time?",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"invalid_event_name_variables":"There is an invalid variable in your event name",
|
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||||
"select_all": "Select All",
|
"select_all": "Select All",
|
||||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||||
"default_conferencing_bulk_description": "Update the locations for the selected event types"
|
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||||
|
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||||
|
"looking_for_more_analytics": "Looking for more analytics?",
|
||||||
|
"add_filter": "Add filter",
|
||||||
|
"select_user": "Select User",
|
||||||
|
"select_event_type": "Select Event Type",
|
||||||
|
"select_date_range": "Select Date Range",
|
||||||
|
"popular_events": "Popular Events",
|
||||||
|
"no_event_types_found": "No event types found",
|
||||||
|
"average_event_duration": "Average Event Duration",
|
||||||
|
"most_booked_members": "Most Booked Members",
|
||||||
|
"least_booked_members": "Least Booked Members",
|
||||||
|
"events_created": "Events Created",
|
||||||
|
"events_completed": "Events Completed",
|
||||||
|
"events_cancelled": "Events Cancelled",
|
||||||
|
"events_rescheduled": "Events Rescheduled",
|
||||||
|
"from_last_period": "from last period",
|
||||||
|
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||||
|
"analytics_for_organisation": "Analytics for {{organisationName}}",
|
||||||
|
"subtitle_analytics": "This is a organisation analytics",
|
||||||
|
"event_trends": "Event Trends"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 46 KiB |
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
|||||||
ctx: {
|
ctx: {
|
||||||
prisma,
|
prisma,
|
||||||
session: null,
|
session: null,
|
||||||
user: null,
|
|
||||||
locale,
|
locale,
|
||||||
i18n: _i18n,
|
i18n: _i18n,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ const base = require("@calcom/config/tailwind-preset");
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
content: [...base.content],
|
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||||
};
|
};
|
||||||
@@ -112,4 +112,25 @@ describe("Tests the slot logic", () => {
|
|||||||
})
|
})
|
||||||
).toHaveLength(11);
|
).toHaveLength(11);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour ", async () => {
|
||||||
|
// 72 20-minutes events in a 24h day
|
||||||
|
expect(
|
||||||
|
getSlots({
|
||||||
|
inviteeDate: dayjs.utc().add(1, "day"),
|
||||||
|
frequency: 20,
|
||||||
|
minimumBookingNotice: 0,
|
||||||
|
workingHours: [
|
||||||
|
{
|
||||||
|
userId: 1,
|
||||||
|
days: Array.from(Array(7).keys()),
|
||||||
|
startTime: MINUTES_DAY_START,
|
||||||
|
endTime: MINUTES_DAY_END - 14, // 23:45
|
||||||
|
},
|
||||||
|
],
|
||||||
|
eventLength: 20,
|
||||||
|
organizerTimeZone: "America/Toronto",
|
||||||
|
})
|
||||||
|
).toHaveLength(71);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"city-timezones": "^1.2.1",
|
"city-timezones": "^1.2.1",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"turbo": "^1.4.3"
|
"turbo": "^1.8.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@apidevtools/json-schema-ref-parser": "9.0.9",
|
"@apidevtools/json-schema-ref-parser": "9.0.9",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function CloseComSetup() {
|
|||||||
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
||||||
|
|
||||||
<div className="mt-1 text-sm">
|
<div className="mt-1 text-sm">
|
||||||
{t("generate_api_key_description")}{" "}
|
{t("generate_api_key_description", { appName: "Close.com" })}{" "}
|
||||||
<a
|
<a
|
||||||
className="text-indigo-400"
|
className="text-indigo-400"
|
||||||
href="https://app.close.com/settings/api/"
|
href="https://app.close.com/settings/api/"
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ items:
|
|||||||
- /api/app-store/dailyvideo/3.jpg
|
- /api/app-store/dailyvideo/3.jpg
|
||||||
---
|
---
|
||||||
|
|
||||||
|
- **Recordings require a team plan**
|
||||||
|
|
||||||
Cal Video is the in-house web-based video conferencing platform powered by Daily.co, which is minimalistic and lightweight, but has most of the features you need.
|
Cal Video is the in-house web-based video conferencing platform powered by Daily.co, which is minimalistic and lightweight, but has most of the features you need.
|
||||||
@@ -74,6 +74,12 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
|
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -106,6 +112,11 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
for (let i = 0; i < event.attendees.length; i++) {
|
for (let i = 0; i < event.attendees.length; i++) {
|
||||||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
||||||
}
|
}
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
appointment.Update(
|
appointment.Update(
|
||||||
ConflictResolutionMode.AlwaysOverwrite,
|
ConflictResolutionMode.AlwaysOverwrite,
|
||||||
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
|
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
|
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -106,6 +112,11 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
for (let i = 0; i < event.attendees.length; i++) {
|
for (let i = 0; i < event.attendees.length; i++) {
|
||||||
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
||||||
}
|
}
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
appointment.Update(
|
appointment.Update(
|
||||||
ConflictResolutionMode.AlwaysOverwrite,
|
ConflictResolutionMode.AlwaysOverwrite,
|
||||||
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
|
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
event.attendees.forEach((attendee: Person) => {
|
event.attendees.forEach((attendee: Person) => {
|
||||||
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
||||||
});
|
});
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member: Person) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
return appointment
|
return appointment
|
||||||
.Save(SendInvitationsMode.SendToAllAndSaveCopy)
|
.Save(SendInvitationsMode.SendToAllAndSaveCopy)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -95,6 +100,11 @@ export default class ExchangeCalendarService implements Calendar {
|
|||||||
event.attendees.forEach((attendee: Person) => {
|
event.attendees.forEach((attendee: Person) => {
|
||||||
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
|
||||||
});
|
});
|
||||||
|
if (event.team?.members) {
|
||||||
|
event.team.members.forEach((member) => {
|
||||||
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
||||||
|
});
|
||||||
|
}
|
||||||
return appointment
|
return appointment
|
||||||
.Update(
|
.Update(
|
||||||
ConflictResolutionMode.AlwaysOverwrite,
|
ConflictResolutionMode.AlwaysOverwrite,
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
...rest,
|
...rest,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
}));
|
}));
|
||||||
|
// TODO: Check every other CalendarService for team members
|
||||||
|
const teamMembers =
|
||||||
|
calEventRaw.team?.members.map((m) => ({
|
||||||
|
email: m.email,
|
||||||
|
displayName: m.name,
|
||||||
|
responseStatus: "accepted",
|
||||||
|
})) || [];
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const myGoogleAuth = await this.auth.getToken();
|
const myGoogleAuth = await this.auth.getToken();
|
||||||
const payload: calendar_v3.Schema$Event = {
|
const payload: calendar_v3.Schema$Event = {
|
||||||
@@ -99,11 +106,12 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
: calEventRaw.organizer.email,
|
: calEventRaw.organizer.email,
|
||||||
},
|
},
|
||||||
...eventAttendees,
|
...eventAttendees,
|
||||||
|
...teamMembers,
|
||||||
],
|
],
|
||||||
reminders: {
|
reminders: {
|
||||||
useDefault: true,
|
useDefault: true,
|
||||||
},
|
},
|
||||||
guestsCanSeeOtherGuests: calEventRaw.seatsShowAttendees,
|
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (calEventRaw.location) {
|
if (calEventRaw.location) {
|
||||||
@@ -125,6 +133,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: selectedCalendar,
|
calendarId: selectedCalendar,
|
||||||
requestBody: payload,
|
requestBody: payload,
|
||||||
conferenceDataVersion: 1,
|
conferenceDataVersion: 1,
|
||||||
|
sendUpdates: "none",
|
||||||
},
|
},
|
||||||
function (error, event) {
|
function (error, event) {
|
||||||
if (error || !event?.data) {
|
if (error || !event?.data) {
|
||||||
@@ -169,6 +178,12 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
...rest,
|
...rest,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
}));
|
}));
|
||||||
|
const teamMembers =
|
||||||
|
event.team?.members.map((m) => ({
|
||||||
|
email: m.email,
|
||||||
|
displayName: m.name,
|
||||||
|
responseStatus: "accepted",
|
||||||
|
})) || [];
|
||||||
const payload: calendar_v3.Schema$Event = {
|
const payload: calendar_v3.Schema$Event = {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: getRichDescription(event),
|
description: getRichDescription(event),
|
||||||
@@ -189,11 +204,12 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
...eventAttendees,
|
...eventAttendees,
|
||||||
|
...teamMembers,
|
||||||
],
|
],
|
||||||
reminders: {
|
reminders: {
|
||||||
useDefault: true,
|
useDefault: true,
|
||||||
},
|
},
|
||||||
guestsCanSeeOtherGuests: event.seatsShowAttendees,
|
guestsCanSeeOtherGuests: !!event.seatsPerTimeSlot ? event.seatsShowAttendees : true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.location) {
|
if (event.location) {
|
||||||
@@ -219,14 +235,13 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: selectedCalendar,
|
calendarId: selectedCalendar,
|
||||||
eventId: uid,
|
eventId: uid,
|
||||||
sendNotifications: true,
|
sendNotifications: true,
|
||||||
sendUpdates: "all",
|
sendUpdates: "none",
|
||||||
requestBody: payload,
|
requestBody: payload,
|
||||||
conferenceDataVersion: 1,
|
conferenceDataVersion: 1,
|
||||||
},
|
},
|
||||||
function (err, evt) {
|
function (err, evt) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
|
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +293,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: calendarId ? calendarId : defaultCalendarId,
|
calendarId: calendarId ? calendarId : defaultCalendarId,
|
||||||
eventId: uid,
|
eventId: uid,
|
||||||
sendNotifications: false,
|
sendNotifications: false,
|
||||||
sendUpdates: "all",
|
sendUpdates: "none",
|
||||||
},
|
},
|
||||||
function (err: GoogleCalError | null, event) {
|
function (err: GoogleCalError | null, event) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -377,16 +377,25 @@ export default class LarkCalendarService implements Calendar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private translateAttendees = (event: CalendarEvent): LarkEventAttendee[] => {
|
private translateAttendees = (event: CalendarEvent): LarkEventAttendee[] => {
|
||||||
const attendees: LarkEventAttendee[] = event.attendees
|
const attendeeArray: LarkEventAttendee[] = [];
|
||||||
|
event.attendees
|
||||||
.filter((att) => att.email)
|
.filter((att) => att.email)
|
||||||
.map((att) => {
|
.forEach((att) => {
|
||||||
const attendee: LarkEventAttendee = {
|
const attendee: LarkEventAttendee = {
|
||||||
type: "third_party",
|
type: "third_party",
|
||||||
is_optional: false,
|
is_optional: false,
|
||||||
third_party_email: att.email,
|
third_party_email: att.email,
|
||||||
};
|
};
|
||||||
return attendee;
|
attendeeArray.push(attendee);
|
||||||
});
|
});
|
||||||
return attendees;
|
event.team?.members.forEach((member) => {
|
||||||
|
const attendee: LarkEventAttendee = {
|
||||||
|
type: "third_party",
|
||||||
|
is_optional: false,
|
||||||
|
third_party_email: member.email,
|
||||||
|
};
|
||||||
|
attendeeArray.push(attendee);
|
||||||
|
});
|
||||||
|
return attendeeArray;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
|
import type { Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
@@ -38,6 +39,14 @@ interface IBatchResponse {
|
|||||||
responses: ISettledResponse[];
|
responses: ISettledResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshTokenResponseSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
expires_in: z
|
||||||
|
.number()
|
||||||
|
.transform((currentTimeOffsetInSeconds) => Math.round(+new Date() / 1000 + currentTimeOffsetInSeconds)),
|
||||||
|
refresh_token: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export default class Office365CalendarService implements Calendar {
|
export default class Office365CalendarService implements Calendar {
|
||||||
private url = "";
|
private url = "";
|
||||||
private integrationName = "";
|
private integrationName = "";
|
||||||
@@ -201,10 +210,16 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private o365Auth = (credential: CredentialPayload) => {
|
private o365Auth = (credential: CredentialPayload) => {
|
||||||
const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000);
|
const isExpired = (expiryDate: number) => {
|
||||||
|
if (!expiryDate) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return expiryDate < Math.round(+new Date() / 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||||
|
|
||||||
const refreshAccessToken = async (refreshToken: string) => {
|
const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
|
||||||
const { client_id, client_secret } = await getOfficeAppKeys();
|
const { client_id, client_secret } = await getOfficeAppKeys();
|
||||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -212,19 +227,22 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||||
client_id,
|
client_id,
|
||||||
refresh_token: refreshToken,
|
refresh_token: o365AuthCredentials.refresh_token,
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
client_secret,
|
client_secret,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const responseBody = await handleErrorsJson<{
|
const responseJson = await handleErrorsJson(response);
|
||||||
access_token: string;
|
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
|
||||||
expires_in: number;
|
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
|
||||||
refresh_token: string;
|
if (!tokenResponse.success) {
|
||||||
}>(response);
|
console.error(
|
||||||
o365AuthCredentials.access_token = responseBody.access_token;
|
"Outlook error grabbing new tokens ~ zodError:",
|
||||||
o365AuthCredentials.refresh_token = responseBody.refresh_token;
|
tokenResponse.error,
|
||||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
"MS response:",
|
||||||
|
responseJson
|
||||||
|
);
|
||||||
|
}
|
||||||
await prisma.credential.update({
|
await prisma.credential.update({
|
||||||
where: {
|
where: {
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
@@ -238,14 +256,14 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getToken: () =>
|
getToken: () =>
|
||||||
!isExpired(o365AuthCredentials.expiry_date)
|
refreshTokenResponseSchema.safeParse(o365AuthCredentials).success &&
|
||||||
|
!isExpired(o365AuthCredentials.expires_in)
|
||||||
? Promise.resolve(o365AuthCredentials.access_token)
|
? Promise.resolve(o365AuthCredentials.access_token)
|
||||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
: refreshAccessToken(o365AuthCredentials),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private translateEvent = (event: CalendarEvent) => {
|
private translateEvent = (event: CalendarEvent) => {
|
||||||
const utcOffset = dayjs(event.startTime, event.organizer.timeZone).utcOffset() / 60;
|
|
||||||
return {
|
return {
|
||||||
subject: event.title,
|
subject: event.title,
|
||||||
body: {
|
body: {
|
||||||
@@ -253,20 +271,31 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
content: getRichDescription(event),
|
content: getRichDescription(event),
|
||||||
},
|
},
|
||||||
start: {
|
start: {
|
||||||
dateTime: dayjs(event.startTime).utcOffset(utcOffset).format(),
|
dateTime: dayjs(event.startTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
||||||
timeZone: event.organizer.timeZone,
|
timeZone: event.organizer.timeZone,
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: dayjs(event.endTime).utcOffset(utcOffset).format(),
|
dateTime: dayjs(event.endTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
||||||
timeZone: event.organizer.timeZone,
|
timeZone: event.organizer.timeZone,
|
||||||
},
|
},
|
||||||
attendees: event.attendees.map((attendee) => ({
|
attendees: [
|
||||||
emailAddress: {
|
...event.attendees.map((attendee) => ({
|
||||||
address: attendee.email,
|
emailAddress: {
|
||||||
name: attendee.name,
|
address: attendee.email,
|
||||||
},
|
name: attendee.name,
|
||||||
type: "required",
|
},
|
||||||
})),
|
type: "required",
|
||||||
|
})),
|
||||||
|
...(event.team?.members
|
||||||
|
? event.team?.members.map((member) => ({
|
||||||
|
emailAddress: {
|
||||||
|
address: member.email,
|
||||||
|
name: member.name,
|
||||||
|
},
|
||||||
|
type: "required",
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
],
|
||||||
location: event.location ? { displayName: getLocation(event) } : undefined,
|
location: event.location ? { displayName: getLocation(event) } : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type O365AuthCredentials = {
|
export type O365AuthCredentials = {
|
||||||
expiry_date: number;
|
|
||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
|
expires_in: number;
|
||||||
};
|
};
|
||||||
@@ -99,23 +99,18 @@ export default function RoutingForms({
|
|||||||
CTA={hasPaidPlan && <NewFormButton />}
|
CTA={hasPaidPlan && <NewFormButton />}
|
||||||
subtitle={t("routing_forms_description")}>
|
subtitle={t("routing_forms_description")}>
|
||||||
<UpgradeTip
|
<UpgradeTip
|
||||||
dark
|
|
||||||
title={t("teams_plan_required")}
|
title={t("teams_plan_required")}
|
||||||
description={t("routing_forms_are_a_great_way")}
|
description={t("routing_forms_are_a_great_way")}
|
||||||
features={features}
|
features={features}
|
||||||
background="/routing-form-banner-background.jpg"
|
background="/banners/routing-forms.jpg"
|
||||||
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
|
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
|
||||||
buttons={
|
buttons={
|
||||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||||
{t("upgrade")}
|
{t("upgrade")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||||
color="minimal"
|
|
||||||
className="!bg-transparent text-white opacity-50 hover:opacity-100"
|
|
||||||
href="https://go.cal.com/teams-video"
|
|
||||||
target="_blank">
|
|
||||||
{t("learn_more")}
|
{t("learn_more")}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function SendgridSetup() {
|
|||||||
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
||||||
|
|
||||||
<div className="mt-1 text-sm">
|
<div className="mt-1 text-sm">
|
||||||
{t("generate_api_key_description")}{" "}
|
{t("generate_api_key_description", { appName: "Sendgrid" })}{" "}
|
||||||
<a
|
<a
|
||||||
className="text-indigo-400"
|
className="text-indigo-400"
|
||||||
href="https://app.sendgrid.com/settings/api_keys"
|
href="https://app.sendgrid.com/settings/api_keys"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Adds a link to copy Typeform Redirect URL to integrate with Routing Forms.
|
description: Adds a link to copy Typeform Redirect URL to integrate with Routing Forms.
|
||||||
items:
|
items:
|
||||||
|
- iframe: { src: https://www.youtube.com/embed/vcV-N2gLPZc }
|
||||||
- /api/app-store/typeform/copy-typeform-redirect-url.png
|
- /api/app-store/typeform/copy-typeform-redirect-url.png
|
||||||
- /api/app-store/typeform/how-it-looks-in-typeform.png
|
- /api/app-store/typeform/how-it-looks-in-typeform.png
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"type": "integrations:whereby_video",
|
"type": "integrations:whereby_video",
|
||||||
"label": "Whereby Video",
|
"label": "Whereby Video",
|
||||||
"organizerInputPlaceholder": "https://www.whereby.com/cal",
|
"organizerInputPlaceholder": "https://www.whereby.com/cal",
|
||||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?(team.)?whereby.com\\/[a-zA-Z0-9]*"
|
"urlRegExp": "^(?:https?://)?(?:(?!.*-\\.)(?:\\w+(-\\w+)*\\.))*whereby\\.com(/[\\w\\-._~:?#\\[\\]@!$&'()*+,;%=]+)*$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,12 +196,11 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const recurrence = getRecurrence(event);
|
const recurrence = getRecurrence(event);
|
||||||
const utcOffset = dayjs(event.startTime, event.organizer.timeZone).utcOffset() / 60;
|
|
||||||
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
|
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
|
||||||
return {
|
return {
|
||||||
topic: event.title,
|
topic: event.title,
|
||||||
type: 2, // Means that this is a scheduled meeting
|
type: 2, // Means that this is a scheduled meeting
|
||||||
start_time: dayjs(event.startTime).utcOffset(utcOffset).format(),
|
start_time: dayjs(event.startTime).utc().format(),
|
||||||
duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000,
|
duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000,
|
||||||
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
|
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
|
||||||
timezone: event.organizer.timeZone,
|
timezone: event.organizer.timeZone,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SelectedCalendar } from "@prisma/client";
|
import type { SelectedCalendar } from "@prisma/client";
|
||||||
import _ from "lodash";
|
import { sortBy } from "lodash";
|
||||||
import * as process from "process";
|
import * as process from "process";
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||||
@@ -54,8 +54,8 @@ export const getConnectedCalendars = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const cals = await calendar.listCalendars();
|
const cals = await calendar.listCalendars();
|
||||||
const calendars = _(cals)
|
const calendars = sortBy(
|
||||||
.map((cal) => {
|
cals.map((cal) => {
|
||||||
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
|
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
|
||||||
return {
|
return {
|
||||||
...cal,
|
...cal,
|
||||||
@@ -64,9 +64,9 @@ export const getConnectedCalendars = async (
|
|||||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||||
credentialId,
|
credentialId,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
.sortBy(["primary"])
|
["primary"]
|
||||||
.value();
|
);
|
||||||
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
||||||
if (!primary) {
|
if (!primary) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { DestinationCalendar, Booking } from "@prisma/client";
|
import type { DestinationCalendar, Booking } from "@prisma/client";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep, merge } from "lodash";
|
||||||
import merge from "lodash/merge";
|
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
@@ -439,12 +438,9 @@ export default class EventManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newBooking) {
|
calendarReference = newBooking?.references.length
|
||||||
calendarReference = newBooking.references.find((reference) => reference.type.includes("_calendar"));
|
? newBooking.references.find((reference) => reference.type.includes("_calendar"))
|
||||||
} else {
|
: booking.references.find((reference) => reference.type.includes("_calendar"));
|
||||||
// Bookings should only have one calendar reference
|
|
||||||
calendarReference = booking.references.find((reference) => reference.type.includes("_calendar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!calendarReference) {
|
if (!calendarReference) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -1,31 +1,6 @@
|
|||||||
/* eslint-disable @calcom/eslint/deprecated-imports */
|
/* eslint-disable @calcom/eslint/deprecated-imports */
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import dayjsBusinessTime from "dayjs-business-days2";
|
import dayjsBusinessTime from "dayjs-business-days2";
|
||||||
import "dayjs/locale/ar";
|
|
||||||
import "dayjs/locale/bg";
|
|
||||||
import "dayjs/locale/cs";
|
|
||||||
import "dayjs/locale/de";
|
|
||||||
import "dayjs/locale/es";
|
|
||||||
import "dayjs/locale/es-mx";
|
|
||||||
import "dayjs/locale/fr";
|
|
||||||
import "dayjs/locale/he";
|
|
||||||
import "dayjs/locale/hu";
|
|
||||||
import "dayjs/locale/it";
|
|
||||||
import "dayjs/locale/ja";
|
|
||||||
import "dayjs/locale/ko";
|
|
||||||
import "dayjs/locale/nl";
|
|
||||||
import "dayjs/locale/pl";
|
|
||||||
import "dayjs/locale/pt";
|
|
||||||
import "dayjs/locale/pt-br";
|
|
||||||
import "dayjs/locale/ro";
|
|
||||||
import "dayjs/locale/ru";
|
|
||||||
import "dayjs/locale/sr";
|
|
||||||
import "dayjs/locale/sv";
|
|
||||||
import "dayjs/locale/tr";
|
|
||||||
import "dayjs/locale/uk";
|
|
||||||
import "dayjs/locale/vi";
|
|
||||||
import "dayjs/locale/zh-cn";
|
|
||||||
import "dayjs/locale/zh-tw";
|
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import isBetween from "dayjs/plugin/isBetween";
|
import isBetween from "dayjs/plugin/isBetween";
|
||||||
import isToday from "dayjs/plugin/isToday";
|
import isToday from "dayjs/plugin/isToday";
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import "dayjs/locale/ar";
|
||||||
|
import "dayjs/locale/bg";
|
||||||
|
import "dayjs/locale/cs";
|
||||||
|
import "dayjs/locale/de";
|
||||||
|
import "dayjs/locale/es";
|
||||||
|
import "dayjs/locale/es-mx";
|
||||||
|
import "dayjs/locale/fr";
|
||||||
|
import "dayjs/locale/he";
|
||||||
|
import "dayjs/locale/hu";
|
||||||
|
import "dayjs/locale/it";
|
||||||
|
import "dayjs/locale/ja";
|
||||||
|
import "dayjs/locale/ko";
|
||||||
|
import "dayjs/locale/nl";
|
||||||
|
import "dayjs/locale/pl";
|
||||||
|
import "dayjs/locale/pt";
|
||||||
|
import "dayjs/locale/pt-br";
|
||||||
|
import "dayjs/locale/ro";
|
||||||
|
import "dayjs/locale/ru";
|
||||||
|
import "dayjs/locale/sr";
|
||||||
|
import "dayjs/locale/sv";
|
||||||
|
import "dayjs/locale/tr";
|
||||||
|
import "dayjs/locale/uk";
|
||||||
|
import "dayjs/locale/vi";
|
||||||
|
import "dayjs/locale/zh-cn";
|
||||||
|
import "dayjs/locale/zh-tw";
|
||||||
@@ -2,6 +2,8 @@ import type { TFunction } from "next-i18next";
|
|||||||
import { RRule } from "rrule";
|
import { RRule } from "rrule";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||||
|
import "@calcom/dayjs/locales";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ export const BaseScheduledEmail = (
|
|||||||
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
||||||
{props.calEvent.cancellationReason && (
|
{props.calEvent.cancellationReason && (
|
||||||
<Info
|
<Info
|
||||||
label={t("cancellation_reason")}
|
label={t(
|
||||||
|
props.calEvent.cancellationReason.startsWith("$RCH$")
|
||||||
|
? "reschedule_reason"
|
||||||
|
: "cancellation_reason"
|
||||||
|
)}
|
||||||
description={
|
description={
|
||||||
!!props.calEvent.cancellationReason && props.calEvent.cancellationReason.replace("$RCH$", "")
|
!!props.calEvent.cancellationReason && props.calEvent.cancellationReason.replace("$RCH$", "")
|
||||||
} // Removing flag to distinguish reschedule from cancellation
|
} // Removing flag to distinguish reschedule from cancellation
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
declare let global: {
|
declare let global: {
|
||||||
E2E_EMAILS?: Record<string, unknown>[];
|
E2E_EMAILS?: Record<string, unknown>[];
|
||||||
@@ -29,7 +31,13 @@ export default class BaseEmail {
|
|||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
public sendEmail() {
|
public async sendEmail() {
|
||||||
|
const featureFlags = await getFeatureFlagMap(prisma);
|
||||||
|
/** If email kill switch exists and is active, we prevent emails being sent. */
|
||||||
|
if (featureFlags.emails) {
|
||||||
|
console.warn("Skipped Sending Email due to active Kill Switch");
|
||||||
|
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
|
||||||
|
}
|
||||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||||
global.E2E_EMAILS = global.E2E_EMAILS || [];
|
global.E2E_EMAILS = global.E2E_EMAILS || [];
|
||||||
global.E2E_EMAILS.push(this.getNodeMailerPayload());
|
global.E2E_EMAILS.push(this.getNodeMailerPayload());
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
|||||||
- Why do we NOT want to provide completely flexible CSS customization by adding whatever CSS user wants. ?
|
- Why do we NOT want to provide completely flexible CSS customization by adding whatever CSS user wants. ?
|
||||||
- Feature Documentation
|
- Feature Documentation
|
||||||
- Inline mode doesn't cause any scroll in iframe by default. It looks like it is part of the website.
|
- Inline mode doesn't cause any scroll in iframe by default. It looks like it is part of the website.
|
||||||
- docs.cal.com
|
- cal.com/docs
|
||||||
|
|
||||||
- A complete document on how to use embed
|
- A complete document on how to use embed
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ async function selectFirstAvailableTimeSlotNextMonth(frame: Frame, page: Page) {
|
|||||||
|
|
||||||
// Waiting for full month increment
|
// Waiting for full month increment
|
||||||
await frame.waitForTimeout(1000);
|
await frame.waitForTimeout(1000);
|
||||||
expect(await page.screenshot()).toMatchSnapshot("availability-page-2.png");
|
// expect(await page.screenshot()).toMatchSnapshot("availability-page-2.png");
|
||||||
// TODO: Find out why the first day is always booked on tests
|
// TODO: Find out why the first day is always booked on tests
|
||||||
await frame.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
await frame.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
||||||
await frame.click('[data-testid="time"]');
|
await frame.click('[data-testid="time"]');
|
||||||
@@ -104,7 +104,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
|
|||||||
// This doesn't seem to be replicable with the speed of a person, only during automation.
|
// This doesn't seem to be replicable with the speed of a person, only during automation.
|
||||||
// It would also allow correct snapshot to be taken for current month.
|
// It would also allow correct snapshot to be taken for current month.
|
||||||
await frame.waitForTimeout(1000);
|
await frame.waitForTimeout(1000);
|
||||||
expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png");
|
// expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png");
|
||||||
const eventSlug = new URL(frame.url()).pathname;
|
const eventSlug = new URL(frame.url()).pathname;
|
||||||
await selectFirstAvailableTimeSlotNextMonth(frame, page);
|
await selectFirstAvailableTimeSlotNextMonth(frame, page);
|
||||||
await frame.waitForNavigation({
|
await frame.waitForNavigation({
|
||||||
@@ -112,7 +112,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
|
|||||||
return url.pathname.includes(`/${username}/book`);
|
return url.pathname.includes(`/${username}/book`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(await page.screenshot()).toMatchSnapshot("booking-page.png");
|
// expect(await page.screenshot()).toMatchSnapshot("booking-page.png");
|
||||||
// --- fill form
|
// --- fill form
|
||||||
await frame.fill('[name="name"]', "Embed User");
|
await frame.fill('[name="name"]', "Embed User");
|
||||||
await frame.fill('[name="email"]', "embed-user@example.com");
|
await frame.fill('[name="email"]', "embed-user@example.com");
|
||||||
@@ -123,7 +123,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
|
|||||||
|
|
||||||
// Make sure we're navigated to the success page
|
// Make sure we're navigated to the success page
|
||||||
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
expect(await page.screenshot()).toMatchSnapshot("success-page.png");
|
// expect(await page.screenshot()).toMatchSnapshot("success-page.png");
|
||||||
|
|
||||||
//NOTE: frame.click('body') won't work here. Because the way it works, it clicks on the center of the body tag which is an element inside the popup view and that won't close the popup
|
//NOTE: frame.click('body') won't work here. Because the way it works, it clicks on the center of the body tag which is an element inside the popup view and that won't close the popup
|
||||||
await frame.evaluate(() => {
|
await frame.evaluate(() => {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ test.describe("Popup Tests", () => {
|
|||||||
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
|
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
|
||||||
pathname: "/free",
|
pathname: "/free",
|
||||||
});
|
});
|
||||||
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
|
// expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
|
||||||
if (!embedIframe) {
|
if (!embedIframe) {
|
||||||
throw new Error("Embed iframe not found");
|
throw new Error("Embed iframe not found");
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 224 KiB |