Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d242add9 | ||
|
|
c272e281cf | ||
|
|
d4e19b0fa4 | ||
|
|
c12877a58f | ||
|
|
af83350d04 | ||
|
|
9d0cf6be0b | ||
|
|
2fe9c782d4 | ||
|
|
d0a3d8d144 | ||
|
|
cefcb1eaf8 | ||
|
|
83b60de640 | ||
|
|
a076418989 | ||
|
|
64ecce1ccb | ||
|
|
cd56a5d4f0 | ||
|
|
d5ca121ac6 | ||
|
|
1e20e80b65 | ||
|
|
75ac3fd40c | ||
|
|
3b91cc7f02 | ||
|
|
23450b61e0 | ||
|
|
dcc226976c | ||
|
|
196fde72f6 | ||
|
|
1e913cd406 | ||
|
|
02425702d1 | ||
|
|
2494655369 | ||
|
|
0e0a78c47e | ||
|
|
ed02c10f54 | ||
|
|
9c40f76184 | ||
|
|
8eda30d88c | ||
|
|
9de19d35a8 | ||
|
|
0770403ca5 | ||
|
|
1a45cb35c1 | ||
|
|
15cdff76d9 | ||
|
|
8c4eb4b5fe | ||
|
|
ae30aafafe | ||
|
|
3b74717591 | ||
|
|
7a6349b4c0 | ||
|
|
3b51fff88d | ||
|
+5 |
2a0b0be9d0 | ||
|
|
f373c9d818 | ||
|
|
540bf3a1cb | ||
|
|
2d9064e92f | ||
|
|
9487819ec1 | ||
|
|
49bf14b196 | ||
|
|
344f16d6cc | ||
|
|
ccbf3b1e54 | ||
|
|
dcf0fad35d | ||
|
|
a836800c69 | ||
|
|
b8c6ac43f7 | ||
|
|
f5e0c6b384 | ||
|
|
2a5dd9035f | ||
|
|
2cddad4336 | ||
|
|
4bcbff51b1 | ||
|
|
ae541e4c37 | ||
|
|
f254711d85 | ||
|
|
533e208643 | ||
|
|
795574ef88 | ||
|
|
48611d5c99 | ||
|
|
11a07961d0 | ||
|
|
6be0fd52e7 | ||
|
|
78da7ba338 | ||
|
|
9a72c648c5 | ||
|
|
3e42da7fc7 | ||
|
|
1d986cad04 | ||
|
|
f603e4eff2 | ||
|
|
1f4e6c1987 | ||
|
|
e57f734e79 | ||
|
|
7fad76ae23 | ||
|
|
7659a016b4 | ||
|
|
34fe1c4a95 | ||
|
|
205f73c4bc | ||
|
|
c4bd2167e1 | ||
|
|
49a7111b66 | ||
|
|
ca045a040b | ||
|
|
a8b4198bf7 | ||
|
|
2ab24e530b | ||
|
|
734b06dfdf | ||
|
|
467084993b | ||
|
|
1e2c8516d2 | ||
|
|
d57738f325 | ||
|
|
c39a7dba85 | ||
|
|
fd37397e5c | ||
|
|
df0a522656 | ||
|
|
916468b8cc | ||
|
|
b789e58624 | ||
|
|
4368536141 | ||
|
|
9eef9766ce | ||
|
|
68dbfbd7d0 | ||
|
|
692a84a86e | ||
|
|
1671095c94 | ||
|
|
3b896bee5c | ||
|
|
cc1985442e | ||
|
|
630f438b32 | ||
|
|
6fae01d45a | ||
|
|
7009373b11 | ||
|
|
c2f150dbab | ||
|
|
688541923b | ||
|
|
20402ac26c | ||
|
|
a54952b41a | ||
|
|
987756c4d4 | ||
|
|
3ee294ffc5 | ||
|
|
a638708871 | ||
|
|
3456cc5521 | ||
|
|
546f0c3881 | ||
|
|
7b18272d60 | ||
|
|
cf6efabdbb | ||
|
|
92d15a00f5 | ||
|
|
2f2b72dd54 | ||
|
|
8fd5d6b5b5 | ||
|
|
2078f12a85 | ||
|
|
77b8eb8a7f | ||
|
|
463787b607 | ||
|
|
1f4e7ca695 | ||
|
|
9eb52bbfb2 | ||
|
|
61e6f76bc6 | ||
|
|
2e6c3a6068 | ||
|
|
8f48ecafec | ||
|
|
8849d1148c | ||
|
|
98a4684a2b | ||
|
|
da0bdfacae | ||
|
|
68c7425922 | ||
|
|
c3418cbe1f | ||
|
|
3a641e0bc1 | ||
|
|
b7fcf6ade1 | ||
|
|
53d160e495 | ||
|
|
df4df6846b | ||
|
|
c1bbb9b139 | ||
|
|
56c07ec351 | ||
|
|
3080b47aec | ||
|
|
c1e23bfdc6 | ||
|
|
e7ff5508d7 | ||
|
|
5850d227ee | ||
|
|
ede987ba19 | ||
|
|
bc0d83164c | ||
|
|
30f0b080c5 | ||
|
|
ee39ea316f | ||
|
|
fbc2708259 | ||
|
|
9bb83aba7e | ||
|
|
3a67e5617e | ||
|
|
f401aa7669 | ||
|
|
a6e0678e59 | ||
|
|
e832015f26 | ||
|
|
5e86881e15 | ||
|
|
3b71f98460 | ||
|
|
144d57adb2 | ||
|
|
e1f1c15318 | ||
|
|
d88b1b2f8f | ||
|
|
2e61c15f2c | ||
|
|
df73473c7f | ||
|
|
7167973afd | ||
|
|
3c17bc2d1a | ||
|
|
a9a295dc54 | ||
|
+2 |
21dd1f4e95 | ||
|
|
5d775bd4a3 | ||
|
|
4e02b22472 | ||
|
|
4686d3ff48 | ||
|
|
02e9d8d45f | ||
|
|
7ce5096f13 | ||
|
|
83e51e9b9d | ||
|
|
dca3278c40 | ||
|
|
7501635fbf | ||
|
|
80c96ea5ac | ||
|
|
8c001e0d34 | ||
|
|
bf275497a2 | ||
|
|
6e351a4b20 | ||
|
|
5360cea3bf | ||
|
+2 |
2f36985462 | ||
|
|
6b26e9a7de | ||
|
|
d7655f0521 | ||
|
|
f4ed345a87 | ||
|
|
c3122c82e4 | ||
|
|
cc9b49131e | ||
|
|
190db1a0f6 | ||
|
|
bd48a72e99 | ||
|
|
a6836a1488 | ||
|
|
2b67ea9635 | ||
|
|
a9117c2d36 | ||
|
+1 |
10d3b5f1b9 | ||
|
|
b757cb6ba1 | ||
|
|
5968e0aa19 | ||
|
|
a06fffc4ec | ||
|
|
3312c84e7c | ||
|
|
d86710d5e9 | ||
|
|
2d8e65c160 | ||
|
|
f74eaf7cb3 | ||
|
|
bcfd57c953 | ||
|
|
c38588b93d | ||
|
|
32ed43574d | ||
|
|
38a5a22b9e | ||
|
|
2120e301c3 | ||
|
|
dd0aec4e97 | ||
|
|
3815b86258 | ||
|
|
7a6545bd03 | ||
|
|
11f467af5d | ||
|
|
0930cf50b6 | ||
|
|
c37cddd0b7 | ||
|
|
09253f83bf | ||
|
|
1cdcebac01 | ||
|
|
58270fa653 | ||
|
|
1a26a254ab | ||
|
|
a36d15894d | ||
|
|
cf077fd33c | ||
|
|
0c979d3604 | ||
|
|
33bc786bb3 | ||
|
|
a7f16462db | ||
|
|
2c7fcbcf51 | ||
|
|
d3e4d26fa5 | ||
|
|
3be6c05722 | ||
|
|
e293e590bd | ||
|
+1 |
d8c1c103a1 | ||
|
|
19d00e691c | ||
|
|
78aa4aecef | ||
|
|
3ab002e547 | ||
|
|
7f461bc275 | ||
|
|
571176102e | ||
|
|
8f1d3fbc50 | ||
|
|
5e88ad50ea |
No files matched your search
+5
-3
@@ -7,14 +7,13 @@
|
|||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE *************************************************************************************************
|
||||||
# Set this value to 'agree' to accept our license:
|
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||||
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
|
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
#
|
||||||
# To enable enterprise-only features, fill your license key in here.
|
# To enable enterprise-only features, fill your license key in here.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
@@ -86,6 +85,9 @@ SENDGRID_EMAIL=
|
|||||||
TWILIO_SID=
|
TWILIO_SID=
|
||||||
TWILIO_TOKEN=
|
TWILIO_TOKEN=
|
||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
|
TWILIO_PHONE_NUMBER=
|
||||||
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
|
TWILIO_VERIFY_SID=
|
||||||
|
|
||||||
# This is used so we can bypass emails in auth flows for E2E testing
|
# This is used so we can bypass emails in auth flows for E2E testing
|
||||||
# Set it to "1" if you need to run E2E tests locally
|
# Set it to "1" if you need to run E2E tests locally
|
||||||
|
|||||||
@@ -3,3 +3,10 @@
|
|||||||
|
|
||||||
automerge:
|
automerge:
|
||||||
- apps/web/public/static/locales/**/*.json
|
- apps/web/public/static/locales/**/*.json
|
||||||
|
|
||||||
|
"❗️ migrations":
|
||||||
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
"❗️ .env changes":
|
||||||
|
- .env.example
|
||||||
|
- .env.appStore.example
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Add bugs to Bug Hunting
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-to-project:
|
|
||||||
name: Add issue to project
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/add-to-project@v0.1.0
|
|
||||||
with:
|
|
||||||
project-url: https://github.com/orgs/calcom/projects/9
|
|
||||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
labeled: 🙋🏻♂️help wanted
|
|
||||||
@@ -12,8 +12,5 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v4
|
||||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
|
||||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
|
||||||
if: ${{ github.actor == 'github-actions[bot]' }}
|
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
@@ -292,7 +292,7 @@ You can deploy Cal on [Railway](https://railway.app/) using the button above. Th
|
|||||||
|
|
||||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
@@ -419,14 +419,18 @@ following
|
|||||||
2. Click ‘Get a Twilio phone number’
|
2. Click ‘Get a Twilio phone number’
|
||||||
3. Copy Account SID to your .env file into the TWILIO_SID field
|
3. Copy Account SID to your .env file into the TWILIO_SID field
|
||||||
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
||||||
5. Create a messaging service (Develop -> Messaging -> Services)
|
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
|
||||||
6. Choose any name for the messaging service
|
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
|
||||||
7. Click 'Add Senders'
|
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||||
8. Choose phone number as sender type
|
8. Choose any name for the messaging service
|
||||||
9. Add the listed phone number
|
9. Click 'Add Senders'
|
||||||
10. Leave all other fields as they are
|
10. Choose phone number as sender type
|
||||||
11. Complete setup and click ‘View my new Messaging Service’
|
11. Add the listed phone number
|
||||||
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
12. Leave all other fields as they are
|
||||||
|
13. Complete setup and click ‘View my new Messaging Service’
|
||||||
|
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||||
|
15. Create a verify service
|
||||||
|
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,6 @@
|
|||||||
"generator": "secret",
|
"generator": "secret",
|
||||||
"required": "true"
|
"required": "true"
|
||||||
},
|
},
|
||||||
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
|
||||||
"description": "License consent",
|
|
||||||
"value": "true",
|
|
||||||
"required": "true"
|
|
||||||
},
|
|
||||||
"CRON_API_KEY": {
|
"CRON_API_KEY": {
|
||||||
"description": "ApiKey for cronjobs",
|
"description": "ApiKey for cronjobs",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
|||||||
+1
-1
Submodule apps/api updated: 055699f612...604d937661.
+1
-1
Submodule apps/console updated: 9174a0c261...2d577dd9ed.
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
stories: [
|
stories: [
|
||||||
"../intro.stories.mdx",
|
"../intro.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
"../../../packages/ui/components/**/*.stories.mdx",
|
||||||
|
"../../../packages/features/**/*.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
@@ -50,6 +51,21 @@ module.exports = {
|
|||||||
vm: false,
|
vm: false,
|
||||||
zlib: false,
|
zlib: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
modules: true, // Enable modules to help you using className
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: path.resolve(__dirname, "../src"),
|
||||||
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -15,7 +15,11 @@ export const parameters = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
|
addDecorator((storyFn) => (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||||
|
</I18nextProvider>
|
||||||
|
));
|
||||||
|
|
||||||
window.getEmbedNamespace = () => {
|
window.getEmbedNamespace = () => {
|
||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||||
|
|
||||||
.sbdocs {
|
.sbdocs {
|
||||||
font-family: 'Inter var' !important;
|
font-family: 'Inter var' !important;
|
||||||
padding: 0!important;
|
padding: 0!important;
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
|
|
||||||
/** Docs table **/
|
/** Docs table **/
|
||||||
.custom-args-wrapper{
|
.custom-args-wrapper{
|
||||||
max-height: 600px;
|
max-height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -187,3 +189,7 @@
|
|||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-story {
|
||||||
|
padding: 24px !important;
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ type PreviewState = {
|
|||||||
palette: {
|
palette: {
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
@@ -125,10 +126,12 @@ const getEmbedUIInstructionString = ({
|
|||||||
apiName,
|
apiName,
|
||||||
theme,
|
theme,
|
||||||
brandColor,
|
brandColor,
|
||||||
|
hideEventTypeDetails,
|
||||||
}: {
|
}: {
|
||||||
apiName: string;
|
apiName: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
theme = theme !== "auto" ? theme : undefined;
|
theme = theme !== "auto" ? theme : undefined;
|
||||||
return getInstructionString({
|
return getInstructionString({
|
||||||
@@ -141,6 +144,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
brandColor,
|
brandColor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -253,18 +257,21 @@ const getEmbedTypeSpecificString = ({
|
|||||||
apiName: string;
|
apiName: string;
|
||||||
theme: PreviewState["theme"];
|
theme: PreviewState["theme"];
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "cal",
|
apiName: "cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "Cal",
|
apiName: "Cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!frameworkCodes[embedType]) {
|
if (!frameworkCodes[embedType]) {
|
||||||
@@ -689,6 +696,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
theme: Theme.auto,
|
theme: Theme.auto,
|
||||||
floatingPopup: {},
|
floatingPopup: {},
|
||||||
elementClick: {},
|
elementClick: {},
|
||||||
|
hideEventTypeDetails: false,
|
||||||
palette: {
|
palette: {
|
||||||
brandColor: "#000000",
|
brandColor: "#000000",
|
||||||
},
|
},
|
||||||
@@ -753,6 +761,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
name: "ui",
|
name: "ui",
|
||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
...previewState.palette,
|
...previewState.palette,
|
||||||
@@ -811,7 +820,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||||
<div className="max-h-97 flex flex-col overflow-y-auto">
|
<div className="flex flex-col overflow-y-auto">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
@@ -838,7 +847,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="text-sm">Embed Window Sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center">
|
||||||
<TextField
|
<TextField
|
||||||
name="width"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
required
|
required
|
||||||
value={previewState.inline.width}
|
value={previewState.inline.width}
|
||||||
@@ -860,7 +868,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<span className="p-2">×</span>
|
<span className="p-2">×</span>
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
name="height"
|
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -888,7 +895,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className="mb-2 text-sm">Button Text</div>
|
<div className="mb-2 text-sm">Button Text</div>
|
||||||
{/* Default Values should come from preview iframe */}
|
{/* Default Values should come from preview iframe */}
|
||||||
<TextField
|
<TextField
|
||||||
name="buttonText"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -1004,6 +1010,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
|
<div className="mb-4 flex items-center justify-start space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={previewState.hideEventTypeDetails}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
hideEventTypeDetails: checked,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||||
|
</div>
|
||||||
<Label className="">
|
<Label className="">
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const Component = ({
|
|||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.setupPending) return;
|
if (data?.setupPending) return;
|
||||||
showToast(t("app_successfully_installed"), "success");
|
showToast(t("app_successfully_installed"), "success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -66,7 +66,9 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||||
|
// Such apps, can only be installed once.
|
||||||
|
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||||
|
|
||||||
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">
|
||||||
@@ -195,7 +197,7 @@ const Component = ({
|
|||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||||
<span>
|
<span>
|
||||||
{price === 0 ? (
|
{price === 0 ? (
|
||||||
"Free"
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Intl.NumberFormat("en-US", {
|
{Intl.NumberFormat("en-US", {
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
if (!data.connectedCalendars.length) {
|
if (!data.connectedCalendars.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
@@ -168,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
slug={item.integration.slug}
|
slug={item.integration.slug}
|
||||||
title={item.integration.title}
|
title={item.integration.title}
|
||||||
logo={item.integration.logo}
|
logo={item.integration.logo}
|
||||||
description={item.primary?.externalId || "No external Id"}
|
description={item.primary?.email ?? item.integration.description}
|
||||||
separate={true}
|
separate={true}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-32 justify-end">
|
<div className="flex w-32 justify-end">
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function IntegrationListItem(props: {
|
|||||||
expanded={!!props.children}
|
expanded={!!props.children}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
||||||
"my-0 flex-col border transition-colors duration-500 ",
|
"my-0 flex-col border transition-colors duration-500",
|
||||||
highlight ? "bg-yellow-100" : ""
|
highlight ? "bg-yellow-100" : ""
|
||||||
)}>
|
)}>
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-bookinglight font-medium">
|
<span className="text-bookinglight font-medium">
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={dayjs(slot.time).format()}>
|
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||||
|
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||||
{/* Current there is no way to disable Next.js Links */}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode, useEffect } from "react";
|
import { FC, ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, Badge } from "@calcom/ui";
|
import { Icon, Badge } from "@calcom/ui";
|
||||||
@@ -41,19 +42,34 @@ interface Props {
|
|||||||
|
|
||||||
const BookingDescription: FC<Props> = (props) => {
|
const BookingDescription: FC<Props> = (props) => {
|
||||||
const { profile, eventType, isBookingPage = false, children } = props;
|
const { profile, eventType, isBookingPage = false, children } = props;
|
||||||
|
const { date: bookingDate } = useRouterQuery("date");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eventType.metadata?.multipleDuration !== undefined) {
|
if (
|
||||||
if (!duration) {
|
eventType.metadata?.multipleDuration &&
|
||||||
setDuration(eventType.length);
|
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||||
} else {
|
) {
|
||||||
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
|
setDuration(eventType.length.toString());
|
||||||
setDuration(eventType.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||||
|
|
||||||
|
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||||
|
let requiresConfirmationText = t("requires_confirmation");
|
||||||
|
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||||
|
if (rcThreshold) {
|
||||||
|
if (isBookingPage) {
|
||||||
|
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
|
||||||
|
requiresConfirmation = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requiresConfirmationText = t("requires_confirmation_threshold", {
|
||||||
|
...rcThreshold,
|
||||||
|
unit: rcThreshold.unit.slice(0, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserAvatars
|
<UserAvatars
|
||||||
@@ -89,16 +105,16 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center",
|
"items-top flex",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
</div>
|
</div>
|
||||||
{t("requires_confirmation")}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AvailableEventLocations
|
<AvailableEventLocations
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
import { ActionType, TableActions } from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
|
||||||
|
|
||||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||||
|
|
||||||
@@ -158,6 +158,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPast && isPending && !isConfirmed) {
|
||||||
|
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||||
|
}
|
||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||||
@@ -248,7 +252,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||||
<td
|
<td
|
||||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
onClick={onClickTableData}>
|
onClick={onClickTableData}>
|
||||||
@@ -275,11 +279,6 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{booking.eventType.team.name}
|
{booking.eventType.team.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
|
||||||
{t("pending_payment")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{booking.paid && (
|
{booking.paid && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{t("paid")}
|
{t("paid")}
|
||||||
@@ -343,12 +342,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
<Badge className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex" variant="orange">
|
||||||
|
{t("pending_payment")}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -531,13 +532,4 @@ const DisplayAttendees = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BookingListItem;
|
export default BookingListItem;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventType } from "@prisma/client";
|
import { EventType } from "@prisma/client";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { TFunction } from "next-i18next";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
@@ -14,12 +13,13 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
|
|||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
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";
|
||||||
@@ -27,15 +27,11 @@ import notEmpty from "@calcom/lib/notEmpty";
|
|||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon, DatePicker } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
// import { timeZone } from "@lib/clock";
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
@@ -48,23 +44,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
// Get router variables
|
|
||||||
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const path = router.asPath.split("/");
|
|
||||||
path.pop(); // Remove the last item (where we currently are)
|
|
||||||
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
|
|
||||||
const slug = path.join("/");
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col justify-end">
|
|
||||||
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
|
||||||
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
|
|
||||||
<p className="sr-only">Go Back</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSlots = ({
|
const useSlots = ({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
eventTypeSlug,
|
eventTypeSlug,
|
||||||
@@ -173,7 +152,10 @@ const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime: browsingDate?.startOf("month"),
|
startTime:
|
||||||
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||||
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||||
|
: browsingDate?.startOf("month"),
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
duration,
|
duration,
|
||||||
@@ -237,7 +219,7 @@ function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
||||||
<p className="flex items-center text-sm font-medium">
|
<p className="flex items-center text-sm font-medium">
|
||||||
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
||||||
{timeZone}
|
{timeZone}
|
||||||
@@ -276,6 +258,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
@@ -300,9 +283,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// TODO: Improve this;
|
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
|
||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -315,7 +295,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
// get dynamic user list here
|
// get dynamic user list here
|
||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
|
|
||||||
@@ -328,6 +308,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
|
|
||||||
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
const gates = [
|
const gates = [
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||||
@@ -361,7 +343,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex-col md:mx-4 lg:flex",
|
"flex flex-col md:mx-4",
|
||||||
shouldAlignCentrally ? "items-center" : "items-start",
|
shouldAlignCentrally ? "items-center" : "items-start",
|
||||||
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
||||||
)}>
|
)}>
|
||||||
@@ -376,60 +358,55 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="overflow-hidden md:flex">
|
||||||
<div
|
{showEventTypeDetails && (
|
||||||
className={classNames(
|
<div
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
className={classNames(
|
||||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||||
)}>
|
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
)}>
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
<div className="flex items-start text-sm font-medium">
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
<div className="flex items-start text-sm font-medium">
|
||||||
<div>
|
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<div>
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
</p>
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
<input
|
</p>
|
||||||
type="number"
|
<input
|
||||||
min="1"
|
type="number"
|
||||||
max={eventType.recurringEvent.count}
|
min="1"
|
||||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
max={eventType.recurringEvent.count}
|
||||||
defaultValue={eventType.recurringEvent.count}
|
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
||||||
onChange={(event) => {
|
defaultValue={eventType.recurringEvent.count}
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
onChange={(event) => {
|
||||||
}}
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
/>
|
}}
|
||||||
<p className="inline">
|
/>
|
||||||
{t("occurrence", {
|
<p className="inline">
|
||||||
count: recurringEventCount,
|
{t("occurrence", {
|
||||||
})}
|
count: recurringEventCount,
|
||||||
</p>
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{stripeAppData.price > 0 && (
|
||||||
{stripeAppData.price > 0 && (
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<IntlProvider locale="en">
|
||||||
<IntlProvider locale="en">
|
<FormattedNumber
|
||||||
<FormattedNumber
|
value={stripeAppData.price / 100.0}
|
||||||
value={stripeAppData.price / 100.0}
|
style="currency"
|
||||||
style="currency"
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
/>
|
||||||
/>
|
</IntlProvider>
|
||||||
</IntlProvider>
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
{timezoneDropdown}
|
||||||
{timezoneDropdown}
|
</BookingDescription>
|
||||||
</BookingDescription>
|
|
||||||
|
|
||||||
{!isEmbed && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div className="mt-auto hidden md:block">
|
|
||||||
<GoBackToPreviousPage t={t} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
||||||
@@ -442,7 +419,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)*/}
|
)*/}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<SlotPicker
|
<SlotPicker
|
||||||
weekStart={
|
weekStart={
|
||||||
typeof profile.weekStart === "string"
|
typeof profile.weekStart === "string"
|
||||||
@@ -468,7 +446,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
||||||
|
{!restProps.isBrandingHidden && !isEmbed && <PoweredByCal />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { LocationObject, LocationType } from "@calcom/core/location";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
@@ -39,11 +40,11 @@ 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 { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
||||||
|
import { Group, RadioField } from "@calcom/ui";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { ensureArray } from "@lib/ensureArray";
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
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";
|
||||||
@@ -89,6 +90,7 @@ const BookingPage = ({
|
|||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { duration: queryDuration } = useRouterQuery("duration");
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -103,7 +105,6 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const stripeAppData = getStripeAppData(eventType);
|
||||||
|
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (queryDuration && !isNaN(Number(queryDuration))) {
|
if (queryDuration && !isNaN(Number(queryDuration))) {
|
||||||
@@ -142,6 +143,7 @@ const BookingPage = ({
|
|||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -157,6 +159,7 @@ const BookingPage = ({
|
|||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -241,7 +244,7 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().trim().email(),
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
@@ -298,6 +301,39 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bookEvent = (booking: BookingFormValues) => {
|
const bookEvent = (booking: BookingFormValues) => {
|
||||||
|
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||||
|
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||||
|
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Checking if custom inputs of type Phone number are valid to display error message on UI
|
||||||
|
if (eventType.customInputs.length) {
|
||||||
|
let isErrorFound = false;
|
||||||
|
eventType.customInputs.forEach((customInput) => {
|
||||||
|
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
|
||||||
|
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
|
||||||
|
try {
|
||||||
|
z.string({
|
||||||
|
errorMap: () => ({
|
||||||
|
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((val) => isValidPhoneNumber(val), {
|
||||||
|
message: "Phone number is invalid",
|
||||||
|
})
|
||||||
|
.parse(input?.value);
|
||||||
|
} catch (err) {
|
||||||
|
isErrorFound = true;
|
||||||
|
bookingForm.setError(`customInputs.${customInput.id}`, {
|
||||||
|
type: "custom",
|
||||||
|
message: "Invalid Phone number",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isErrorFound) return;
|
||||||
|
}
|
||||||
|
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||||
{ isTeamBooking: document.URL.includes("team/") }
|
{ isTeamBooking: document.URL.includes("team/") }
|
||||||
@@ -316,6 +352,22 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (eventType.customInputs.length > 0) {
|
||||||
|
// find all required custom inputs and ensure they are filled out in the booking form
|
||||||
|
const requiredCustomInputs = eventType.customInputs.filter((input) => input.required);
|
||||||
|
const missingRequiredCustomInputs = requiredCustomInputs.filter(
|
||||||
|
(input) => !booking?.customInputs?.[input.id]
|
||||||
|
);
|
||||||
|
if (missingRequiredCustomInputs.length > 0) {
|
||||||
|
missingRequiredCustomInputs.forEach((input) => {
|
||||||
|
bookingForm.setError(`customInputs.${input.id}`, {
|
||||||
|
type: "required",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (recurringDates.length) {
|
if (recurringDates.length) {
|
||||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
@@ -338,10 +390,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -369,10 +418,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -407,6 +453,7 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
@@ -440,7 +487,7 @@ const BookingPage = ({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? "" : "sm:my-24",
|
isEmbed ? "" : "sm:my-24",
|
||||||
"my-0 max-w-3xl "
|
"my-0 max-w-3xl"
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -449,95 +496,97 @@ const BookingPage = ({
|
|||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"dark:border-darkgray-300 rounded-md sm:border"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
{showEventTypeDetails && (
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
{stripeAppData.price > 0 && (
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
{stripeAppData.price > 0 && (
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<IntlProvider locale="en">
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<FormattedNumber
|
<IntlProvider locale="en">
|
||||||
value={stripeAppData.price / 100.0}
|
<FormattedNumber
|
||||||
style="currency"
|
value={stripeAppData.price / 100.0}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
style="currency"
|
||||||
/>
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
</IntlProvider>
|
/>
|
||||||
</p>
|
</IntlProvider>
|
||||||
)}
|
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
|
||||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
|
||||||
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
|
||||||
<p className="-ml-2 inline-block items-center px-2">
|
|
||||||
{getEveryFreqFor({
|
|
||||||
t,
|
|
||||||
recurringEvent: eventType.recurringEvent,
|
|
||||||
recurringCount: recurringEventCount,
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||||
<div className="text-sm font-medium">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
{getEveryFreqFor({
|
||||||
{!rescheduleUid &&
|
t,
|
||||||
eventType.recurringEvent?.freq &&
|
recurringEvent: eventType.recurringEvent,
|
||||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
recurringCount: recurringEventCount,
|
||||||
return <p key={key}>{timeFormatted}</p>;
|
})}
|
||||||
})}
|
</p>
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
</div>
|
||||||
<div className="flex">
|
)}
|
||||||
<Tooltip
|
|
||||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
|
||||||
<p key={key}>{timeFormatted}</p>
|
|
||||||
))}>
|
|
||||||
<p className="dark:text-darkgray-600 text-sm">
|
|
||||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{booking?.startTime && rescheduleUid && (
|
|
||||||
<div>
|
|
||||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
|
||||||
{t("former_time")}
|
|
||||||
</p>
|
|
||||||
<p className="line-through ">
|
|
||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<Icon.FiUser
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||||
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
<div className="text-sm font-medium">
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||||
? "text-rose-600"
|
{!rescheduleUid &&
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
eventType.recurringEvent?.freq &&
|
||||||
? "text-yellow-500"
|
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||||
: "text-bookinghighlight"
|
return <p key={key}>{timeFormatted}</p>;
|
||||||
}`}
|
})}
|
||||||
/>
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
<p
|
<div className="flex">
|
||||||
className={`${
|
<Tooltip
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
? "text-rose-600"
|
<p key={key}>{timeFormatted}</p>
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
))}>
|
||||||
? "text-yellow-500"
|
<p className="dark:text-darkgray-600 text-sm">
|
||||||
: "text-bookinghighlight"
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
} mb-2 font-medium`}>
|
</p>
|
||||||
{booking
|
</Tooltip>
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
</div>
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
)}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{booking?.startTime && rescheduleUid && (
|
||||||
</BookingDescription>
|
<div>
|
||||||
</div>
|
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||||
<div className="p-6 sm:w-1/2">
|
{t("former_time")}
|
||||||
|
</p>
|
||||||
|
<p className="line-through ">
|
||||||
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
|
<Icon.FiUser
|
||||||
|
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
} mb-2 font-medium`}>
|
||||||
|
{booking
|
||||||
|
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||||
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BookingDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
|
||||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
@@ -680,8 +729,11 @@ const BookingPage = ({
|
|||||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||||
<label
|
<label
|
||||||
htmlFor={"custom_" + input.id}
|
htmlFor={"custom_" + input.id}
|
||||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
className={classNames(
|
||||||
{input.label}
|
"mb-1 block text-sm font-medium text-gray-700 transition-colors dark:text-white",
|
||||||
|
bookingForm.formState.errors.customInputs?.[input.id] && "!text-red-700"
|
||||||
|
)}>
|
||||||
|
{input.label} {input.required && <span className="text-red-700">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||||
@@ -745,6 +797,48 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
||||||
|
<div className="flex">
|
||||||
|
<Group
|
||||||
|
required={input.required}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
bookingForm.setValue(`customInputs.${input.id}`, e);
|
||||||
|
}}>
|
||||||
|
<>
|
||||||
|
{input.options.map((option, i) => (
|
||||||
|
<RadioField
|
||||||
|
label={option.label}
|
||||||
|
key={`option.${i}.radio`}
|
||||||
|
value={option.label}
|
||||||
|
id={`option.${i}.radio`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
{bookingForm.formState.errors.customInputs?.[input.id] && (
|
||||||
|
<div className="mt-px flex items-center text-xs text-red-700 ">
|
||||||
|
<p>{t("required")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.PHONE && (
|
||||||
|
<div>
|
||||||
|
<PhoneInput<BookingFormValues>
|
||||||
|
name={`customInputs.${input.id}`}
|
||||||
|
control={bookingForm.control}
|
||||||
|
placeholder={t("enter_phone_number")}
|
||||||
|
id={`customInputs.${input.id}`}
|
||||||
|
required={input.required}
|
||||||
|
/>
|
||||||
|
{bookingForm.formState.errors?.customInputs?.[input.id] && (
|
||||||
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
|
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||||
|
<p>{t("invalid_number")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!eventType.disableGuests && guestToggle && (
|
{!eventType.disableGuests && guestToggle && (
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Select<{ label: string; value: string; icon?: string }>
|
<Select<{ label: string; value: string; icon?: string }>
|
||||||
maxMenuHeight={150}
|
maxMenuHeight={300}
|
||||||
name="location"
|
name="location"
|
||||||
defaultValue={selection}
|
defaultValue={selection}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ type AvailabilityOption = {
|
|||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.Option>
|
</components.Option>
|
||||||
@@ -34,12 +35,13 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.SingleValue>
|
</components.SingleValue>
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import { EventTypeCustomInputType } from "@prisma/client";
|
||||||
|
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { Button, Select, TextField } from "@calcom/ui";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
interface OptionTypeBase {
|
||||||
label: string;
|
label: string;
|
||||||
value: EventTypeCustomInputType;
|
value: EventTypeCustomInputType;
|
||||||
|
options?: { label: string; type: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
onSubmit: (output: CustomInputParsed) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
selectedCustomInput?: CustomInputParsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
type IFormInput = CustomInputParsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting a random ID gives us the option to know WHICH field is changed
|
||||||
|
* when the user edits a custom field.
|
||||||
|
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
|
||||||
|
* There is very very very slim chance that this will cause a collision
|
||||||
|
* */
|
||||||
|
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -26,10 +36,23 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
||||||
|
{
|
||||||
|
value: EventTypeCustomInputType.RADIO,
|
||||||
|
label: t("radio"),
|
||||||
|
},
|
||||||
|
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
const { selectedCustomInput } = props;
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
const defaultValues = selectedCustomInput
|
||||||
|
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
|
||||||
|
: {
|
||||||
|
id: randomId(),
|
||||||
|
type: EventTypeCustomInputType.TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { register, control, getValues } = useForm<IFormInput>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
const selectedInputType = useWatch({ name: "type", control });
|
||||||
@@ -40,7 +63,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
||||||
{t("input_type")}
|
{t("input_type")}
|
||||||
@@ -83,6 +106,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{...register("placeholder")}
|
{...register("placeholder")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedInputType === EventTypeCustomInputType.RADIO && (
|
||||||
|
<RadioInputHandler control={control} register={register} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
<input
|
<input
|
||||||
id="required"
|
id="required"
|
||||||
@@ -111,12 +138,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit(props.onSubmit)} form="custom-input">
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onSubmit(getValues());
|
||||||
|
}}>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function RadioInputHandler({
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
}: {
|
||||||
|
register: UseFormRegister<IFormInput>;
|
||||||
|
control: Control<IFormInput>;
|
||||||
|
}) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { fields, append, remove } = useFieldArray<IFormInput>({
|
||||||
|
control,
|
||||||
|
name: "options",
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col ">
|
||||||
|
<Label htmlFor="radio_options">{t("options")}</Label>
|
||||||
|
<ul
|
||||||
|
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
|
||||||
|
ref={animateRef}>
|
||||||
|
<>
|
||||||
|
{fields.map((option, index) => (
|
||||||
|
<li key={`${option.id}`}>
|
||||||
|
<TextField
|
||||||
|
id={option.id}
|
||||||
|
placeholder={t("enter_option", { index: index + 1 })}
|
||||||
|
addOnFilled={false}
|
||||||
|
label={t("option", { index: index + 1 })}
|
||||||
|
labelSrOnly
|
||||||
|
{...register(`options.${index}.label` as const, { required: true })}
|
||||||
|
addOnSuffix={
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiX}
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
className="!text-sm !font-medium"
|
||||||
|
onClick={() => {
|
||||||
|
append({ label: "", type: "text" });
|
||||||
|
}}>
|
||||||
|
{t("add_an_option")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
export default CustomInputTypeForm;
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
import { ErrorMessage } from "@hookform/error-message";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
||||||
import { components } from "react-select";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import {
|
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
|
||||||
getHumanReadableLocationValue,
|
|
||||||
getMessageForOrganizer,
|
|
||||||
LocationObject,
|
|
||||||
LocationType,
|
|
||||||
} from "@calcom/app-store/locations";
|
|
||||||
import { classNames } from "@calcom/lib";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
|
||||||
import { Button, Dialog, DialogContent, Form, Icon, Label, PhoneInput, Select } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
import { LinkText } from "@components/ui/LinkText";
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
|
||||||
|
|
||||||
type OptionTypeBase = {
|
|
||||||
label: string;
|
|
||||||
value: EventLocationType["type"];
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ISetLocationDialog {
|
|
||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
|
||||||
selection?: OptionTypeBase;
|
|
||||||
booking?: BookingItem;
|
|
||||||
defaultValues?: LocationObject[];
|
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isOpenDialog: boolean;
|
|
||||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationInput = (props: {
|
|
||||||
eventLocationType: EventLocationType;
|
|
||||||
locationFormMethods: ReturnType<typeof useForm>;
|
|
||||||
id: string;
|
|
||||||
required: boolean;
|
|
||||||
placeholder: string;
|
|
||||||
className?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
}): JSX.Element | null => {
|
|
||||||
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
|
||||||
return (
|
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
|
||||||
);
|
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
|
||||||
return (
|
|
||||||
<PhoneInput
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
{...remainingProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|
||||||
const {
|
|
||||||
saveLocation,
|
|
||||||
selection,
|
|
||||||
booking,
|
|
||||||
setShowLocationModal,
|
|
||||||
isOpenDialog,
|
|
||||||
defaultValues,
|
|
||||||
setSelectedLocation,
|
|
||||||
} = props;
|
|
||||||
const { t } = useLocale();
|
|
||||||
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selection) {
|
|
||||||
locationFormMethods.setValue("locationType", selection?.value);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selection]);
|
|
||||||
|
|
||||||
const locationFormSchema = z.object({
|
|
||||||
locationType: z.string(),
|
|
||||||
phone: z.string().optional(),
|
|
||||||
locationAddress: z.string().optional(),
|
|
||||||
locationLink: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.superRefine((val, ctx) => {
|
|
||||||
if (
|
|
||||||
eventLocationType &&
|
|
||||||
!eventLocationType.default &&
|
|
||||||
eventLocationType.linkType === "static" &&
|
|
||||||
eventLocationType.urlRegExp
|
|
||||||
) {
|
|
||||||
const valid = z
|
|
||||||
.string()
|
|
||||||
.regex(new RegExp(eventLocationType.urlRegExp || ""))
|
|
||||||
.safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL for ${eventLocationType.label}. ${
|
|
||||||
sampleUrl ? "Sample URL: " + sampleUrl : ""
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = z.string().url().optional().safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
displayLocationPublicly: z.boolean().optional(),
|
|
||||||
locationPhoneNumber: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const locationFormMethods = useForm({
|
|
||||||
mode: "onSubmit",
|
|
||||||
resolver: zodResolver(locationFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedLocation = useWatch({
|
|
||||||
control: locationFormMethods.control,
|
|
||||||
name: "locationType",
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventLocationType = getEventLocationType(selectedLocation);
|
|
||||||
|
|
||||||
const defaultLocation = defaultValues?.find(
|
|
||||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
|
||||||
);
|
|
||||||
|
|
||||||
const LocationOptions = (() => {
|
|
||||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
|
||||||
if (!eventLocationType.variable) {
|
|
||||||
console.error("eventLocationType.variable can't be undefined");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
|
|
||||||
<LocationInput
|
|
||||||
locationFormMethods={locationFormMethods}
|
|
||||||
eventLocationType={eventLocationType}
|
|
||||||
id="locationInput"
|
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
|
||||||
required
|
|
||||||
className="block w-full rounded-md border-gray-300 text-sm"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
defaultValue={
|
|
||||||
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ErrorMessage
|
|
||||||
errors={locationFormMethods.formState.errors}
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
className="mt-1 text-sm text-red-500"
|
|
||||||
as="p"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpenDialog}>
|
|
||||||
<DialogContent
|
|
||||||
type="creation"
|
|
||||||
Icon={Icon.FiMapPin}
|
|
||||||
title={t("edit_location")}
|
|
||||||
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
|
|
||||||
{booking && (
|
|
||||||
<>
|
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Form
|
|
||||||
form={locationFormMethods}
|
|
||||||
className="space-y-4"
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
|
||||||
|
|
||||||
let details = {};
|
|
||||||
if (newLocation === LocationType.AttendeeInPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.InPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const eventLocationType = getEventLocationType(newLocation);
|
|
||||||
|
|
||||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
|
||||||
if (
|
|
||||||
newLocation === LocationType.Link ||
|
|
||||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
|
||||||
) {
|
|
||||||
details = { link: values.locationLink };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLocationType?.organizerInputType) {
|
|
||||||
details = {
|
|
||||||
...details,
|
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLocation(newLocation, details);
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationType",
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
}}>
|
|
||||||
<QueryCell
|
|
||||||
query={locationsQuery}
|
|
||||||
success={({ data: locationOptions }) => {
|
|
||||||
if (!locationOptions.length) return null;
|
|
||||||
if (booking) {
|
|
||||||
locationOptions.forEach((location) => {
|
|
||||||
if (location.label === "phone") {
|
|
||||||
location.options.filter((l) => l.value !== "phone");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name="locationType"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<Select<{ label: string; value: string; icon?: string }>
|
|
||||||
maxMenuHeight={150}
|
|
||||||
name="location"
|
|
||||||
defaultValue={selection}
|
|
||||||
options={locationOptions}
|
|
||||||
components={{
|
|
||||||
Option: (props) => (
|
|
||||||
<components.Option {...props}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{props.data.icon && (
|
|
||||||
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"text-sm font-medium",
|
|
||||||
props.isSelected ? "text-white" : "text-gray-900"
|
|
||||||
)}>
|
|
||||||
{props.data.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</components.Option>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
formatOptionLabel={(e) => (
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
|
|
||||||
<span>{e.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) {
|
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
locationFormMethods.clearErrors([
|
|
||||||
"locationLink",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
"locationAddress",
|
|
||||||
]);
|
|
||||||
setSelectedLocation?.(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{selectedLocation && LocationOptions}
|
|
||||||
<div className="mt-5">
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
|
||||||
Can't find the right video app? Visit our
|
|
||||||
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
|
|
||||||
App Store
|
|
||||||
</LinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister("locationType");
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
color="secondary">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import { EventTypeCustomInput } from "@prisma/client/";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
|
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CustomInputItem,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,6 +27,8 @@ import {
|
|||||||
|
|
||||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||||
|
|
||||||
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
const generateHashedLink = (id: number) => {
|
const generateHashedLink = (id: number) => {
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const seed = `${id}:${new Date().getTime()}`;
|
const seed = `${id}:${new Date().getTime()}`;
|
||||||
@@ -43,11 +44,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
|
||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
@@ -101,7 +103,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
@@ -125,15 +127,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
if (e && customInputs.length === 0) {
|
if (e && customInputs.length === 0) {
|
||||||
// Push a placeholders
|
// Push a placeholders
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
} else if (!e) {
|
} else if (!e) {
|
||||||
setCustomInputs([]);
|
|
||||||
formMethods.setValue("customInputs", []);
|
formMethods.setValue("customInputs", []);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<ul className="my-4 rounded-md border">
|
<ul className="my-4 rounded-md border">
|
||||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
{customInputs.map((customInput, idx) => (
|
||||||
<CustomInputItem
|
<CustomInputItem
|
||||||
key={idx}
|
key={idx}
|
||||||
question={customInput.label}
|
question={customInput.label}
|
||||||
@@ -162,18 +162,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<RequiresConfirmationController
|
||||||
name="requiresConfirmation"
|
seatsEnabled={seatsEnabled}
|
||||||
defaultValue={eventType.requiresConfirmation}
|
metadata={eventType.metadata}
|
||||||
render={({ field: { value, onChange } }) => (
|
requiresConfirmation={requiresConfirmation}
|
||||||
<SettingsToggle
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
title={t("requires_confirmation")}
|
|
||||||
description={t("requires_confirmation_description")}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
disabled={seatsEnabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
@@ -309,6 +302,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
if (e) {
|
if (e) {
|
||||||
formMethods.setValue("disableGuests", true);
|
formMethods.setValue("disableGuests", true);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
@@ -358,7 +352,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
/>
|
/>
|
||||||
@@ -391,24 +385,36 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<CustomInputTypeForm
|
<CustomInputTypeForm
|
||||||
selectedCustomInput={selectedCustomInput}
|
selectedCustomInput={selectedCustomInput}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
const customInput: EventTypeCustomInput = {
|
const customInput: CustomInputParsed = {
|
||||||
id: -1,
|
id: -1,
|
||||||
eventTypeId: -1,
|
eventTypeId: -1,
|
||||||
label: values.label,
|
label: values.label,
|
||||||
placeholder: values.placeholder,
|
placeholder: values.placeholder,
|
||||||
required: values.required,
|
required: values.required,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
|
options: values.options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedCustomInput) {
|
if (selectedCustomInput) {
|
||||||
selectedCustomInput.label = customInput.label;
|
selectedCustomInput.label = customInput.label;
|
||||||
selectedCustomInput.placeholder = customInput.placeholder;
|
selectedCustomInput.placeholder = customInput.placeholder;
|
||||||
selectedCustomInput.required = customInput.required;
|
selectedCustomInput.required = customInput.required;
|
||||||
selectedCustomInput.type = customInput.type;
|
selectedCustomInput.type = customInput.type;
|
||||||
|
selectedCustomInput.options = customInput.options || undefined;
|
||||||
|
// Update by id
|
||||||
|
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
||||||
|
customInputs[inputIndex] = selectedCustomInput;
|
||||||
|
setCustomInputs(customInputs);
|
||||||
|
formMethods.setValue("customInputs", customInputs);
|
||||||
} else {
|
} else {
|
||||||
setCustomInputs(customInputs.concat(customInput));
|
const concatted = customInputs.concat({
|
||||||
formMethods.setValue("customInputs", customInputs.concat(customInput));
|
...customInput,
|
||||||
|
options: customInput.options,
|
||||||
|
});
|
||||||
|
console.log(concatted);
|
||||||
|
setCustomInputs(concatted);
|
||||||
|
formMethods.setValue("customInputs", concatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
setSelectedCustomInputModalOpen(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
|
|||||||
@@ -33,22 +33,10 @@ export const EventSetupTab = (
|
|||||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
const multipleDurationOptions = [
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
{ value: 5, label: t("multiple_duration_mins", { count: 5 }) },
|
value: mins,
|
||||||
{ value: 10, label: t("multiple_duration_mins", { count: 10 }) },
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
{ value: 15, label: t("multiple_duration_mins", { count: 15 }) },
|
}));
|
||||||
{ value: 20, label: t("multiple_duration_mins", { count: 20 }) },
|
|
||||||
{ value: 25, label: t("multiple_duration_mins", { count: 25 }) },
|
|
||||||
{ value: 30, label: t("multiple_duration_mins", { count: 30 }) },
|
|
||||||
{ value: 45, label: t("multiple_duration_mins", { count: 45 }) },
|
|
||||||
{ value: 50, label: t("multiple_duration_mins", { count: 50 }) },
|
|
||||||
{ value: 60, label: t("multiple_duration_mins", { count: 60 }) },
|
|
||||||
{ value: 75, label: t("multiple_duration_mins", { count: 75 }) },
|
|
||||||
{ value: 80, label: t("multiple_duration_mins", { count: 80 }) },
|
|
||||||
{ value: 90, label: t("multiple_duration_mins", { count: 90 }) },
|
|
||||||
{ value: 120, label: t("multiple_duration_mins", { count: 120 }) },
|
|
||||||
{ value: 180, label: t("multiple_duration_mins", { count: 180 }) },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
||||||
MultiValue<{
|
MultiValue<{
|
||||||
@@ -142,6 +130,7 @@ export const EventSetupTab = (
|
|||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
@@ -224,7 +213,7 @@ export const EventSetupTab = (
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("Title")}
|
label={t("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
@@ -261,23 +250,26 @@ export const EventSetupTab = (
|
|||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="h-auto !min-h-[36px] text-sm"
|
className="h-auto !min-h-[36px] text-sm"
|
||||||
options={multipleDurationOptions}
|
options={multipleDurationOptions}
|
||||||
|
value={selectedMultipleDuration}
|
||||||
onChange={(options) => {
|
onChange={(options) => {
|
||||||
const values = options
|
let newOptions = [...options];
|
||||||
.map((opt) => opt.value)
|
newOptions = newOptions.sort((a, b) => {
|
||||||
.sort(function (a, b) {
|
return a?.value - b?.value;
|
||||||
return a - b;
|
});
|
||||||
});
|
const values = newOptions.map((opt) => opt.value);
|
||||||
setMultipleDuration(values);
|
setMultipleDuration(values);
|
||||||
setSelectedMultipleDuration(options);
|
setSelectedMultipleDuration(newOptions);
|
||||||
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
|
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||||
if (options.length > 0) {
|
if (newOptions.length > 0) {
|
||||||
setDefaultDuration(options[0]);
|
setDefaultDuration(newOptions[0]);
|
||||||
|
formMethods.setValue("length", newOptions[0].value);
|
||||||
} else {
|
} else {
|
||||||
setDefaultDuration(null);
|
setDefaultDuration(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.length === 1 && defaultDuration === null) {
|
if (newOptions.length === 1 && defaultDuration === null) {
|
||||||
setDefaultDuration(options[0]);
|
setDefaultDuration(newOptions[0]);
|
||||||
|
formMethods.setValue("length", newOptions[0].value);
|
||||||
}
|
}
|
||||||
formMethods.setValue("metadata.multipleDuration", values);
|
formMethods.setValue("metadata.multipleDuration", values);
|
||||||
}}
|
}}
|
||||||
@@ -298,7 +290,7 @@ export const EventSetupTab = (
|
|||||||
setDefaultDuration(
|
setDefaultDuration(
|
||||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||||
);
|
);
|
||||||
formMethods.setValue("length", Number(option?.value));
|
if (option) formMethods.setValue("length", option.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,14 +298,11 @@ export const EventSetupTab = (
|
|||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
name="length"
|
|
||||||
type="number"
|
type="number"
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
onChange={(e) => {
|
{...formMethods.register("length")}
|
||||||
formMethods.setValue("length", Number(e.target.value));
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
|
import { Avatar, Button, Icon, Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IMemberToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
|
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Webhook } from "@calcom/prisma/client";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
export const EventTeamWebhooksTab = ({
|
||||||
|
eventType,
|
||||||
|
team,
|
||||||
|
}: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||||
|
|
||||||
|
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||||
|
variant: "other",
|
||||||
|
onlyInstalled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
|
||||||
|
|
||||||
|
const subscriberUrlReserved = (subscriberUrl: string, id?: string): boolean => {
|
||||||
|
return !!webhooks?.find(
|
||||||
|
(webhook) => webhook.subscriberUrl === subscriberUrl && (!id || webhook.id !== id)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
await utils.viewer.webhook.list.invalidate();
|
||||||
|
showToast(t("webhook_updated_successfully"), "success");
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast(`${error.message}`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
showToast(t("webhook_created_successfully"), "success");
|
||||||
|
await utils.viewer.webhook.list.invalidate();
|
||||||
|
setCreateModalOpen(false);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast(`${error.message}`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
|
||||||
|
if (subscriberUrlReserved(values.subscriberUrl, values.id)) {
|
||||||
|
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.payloadTemplate) {
|
||||||
|
values.payloadTemplate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebhookMutation.mutate({
|
||||||
|
subscriberUrl: values.subscriberUrl,
|
||||||
|
eventTriggers: values.eventTriggers,
|
||||||
|
active: values.active,
|
||||||
|
payloadTemplate: values.payloadTemplate,
|
||||||
|
secret: values.secret,
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewWebhookButton = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
data-testid="new_webhook"
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
|
{t("new_webhook")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{team && webhooks && !isLoading && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
{webhooks.length ? (
|
||||||
|
<>
|
||||||
|
<div className="mt-4 mb-8 rounded-md border">
|
||||||
|
{webhooks.map((webhook, index) => {
|
||||||
|
return (
|
||||||
|
<WebhookListItem
|
||||||
|
key={webhook.id}
|
||||||
|
webhook={webhook}
|
||||||
|
lastItem={webhooks.length === index + 1}
|
||||||
|
onEditWebhook={() => {
|
||||||
|
setEditModalOpen(true);
|
||||||
|
setWebhookToEdit(webhook);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<NewWebhookButton />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={TbWebhook}
|
||||||
|
headline={t("create_your_first_webhook")}
|
||||||
|
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||||
|
buttonRaw={<NewWebhookButton />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New webhook dialog */}
|
||||||
|
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||||
|
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
|
||||||
|
<WebhookForm
|
||||||
|
onSubmit={onCreateWebhook}
|
||||||
|
onCancel={() => setCreateModalOpen(false)}
|
||||||
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* Edit webhook dialog */}
|
||||||
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
|
<DialogContent title={t("edit_webhook")}>
|
||||||
|
<WebhookForm
|
||||||
|
webhook={webhookToEdit}
|
||||||
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
onCancel={() => setEditModalOpen(false)}
|
||||||
|
onSubmit={(values: WebhookFormSubmitData) => {
|
||||||
|
if (subscriberUrlReserved(values.subscriberUrl, webhookToEdit?.id || "")) {
|
||||||
|
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.changeSecret) {
|
||||||
|
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.payloadTemplate) {
|
||||||
|
values.payloadTemplate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editWebhookMutation.mutate({
|
||||||
|
id: webhookToEdit?.id || "",
|
||||||
|
subscriberUrl: values.subscriberUrl,
|
||||||
|
eventTriggers: values.eventTriggers,
|
||||||
|
active: values.active,
|
||||||
|
payloadTemplate: values.payloadTemplate,
|
||||||
|
secret: values.secret,
|
||||||
|
eventTypeId: webhookToEdit?.eventTypeId || undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,8 +2,8 @@ import { TFunction } from "next-i18next";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Loader } from "react-feather";
|
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -67,7 +67,7 @@ function getNavigation(props: {
|
|||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
icon: Icon.FiCalendar,
|
icon: Icon.FiCalendar,
|
||||||
info: `Working Hours`, // TODO: Get this from props
|
info: `default_schedule_name`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
@@ -150,13 +150,20 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team)
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "scheduling_type",
|
name: "scheduling_type",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: Icon.FiUsers,
|
icon: Icon.FiUsers,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||||
});
|
});
|
||||||
|
navigation.push({
|
||||||
|
name: "webhooks",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
|
icon: TbWebhook,
|
||||||
|
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
||||||
|
|
||||||
@@ -294,7 +301,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<ClientSuspense fallback={<Loader />}>
|
<ClientSuspense fallback={<Icon.FiLoader />}>
|
||||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import { UnitTypeLongPlural } from "dayjs";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
|
type RequiresConfirmationControllerProps = {
|
||||||
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
|
requiresConfirmation: boolean;
|
||||||
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
|
seatsEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RequiresConfirmationController({
|
||||||
|
metadata,
|
||||||
|
requiresConfirmation,
|
||||||
|
onRequiresConfirmation,
|
||||||
|
seatsEnabled,
|
||||||
|
}: RequiresConfirmationControllerProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
|
||||||
|
metadata?.requiresConfirmationThreshold
|
||||||
|
);
|
||||||
|
const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requiresConfirmation) {
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
}
|
||||||
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="block items-start sm:flex">
|
||||||
|
<div className={!seatsEnabled ? "w-full" : ""}>
|
||||||
|
{seatsEnabled ? (
|
||||||
|
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
name="requiresConfirmation"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("requires_confirmation")}
|
||||||
|
description={t("requires_confirmation_description")}
|
||||||
|
checked={requiresConfirmation}
|
||||||
|
onCheckedChange={(val) => {
|
||||||
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
|
onRequiresConfirmation(val);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={
|
||||||
|
requiresConfirmation
|
||||||
|
? requiresConfirmationSetup === undefined
|
||||||
|
? "always"
|
||||||
|
: "notice"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val === "always") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
setRequiresConfirmationSetup(undefined);
|
||||||
|
} else if (val === "notice") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold",
|
||||||
|
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="always"
|
||||||
|
value="always"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="always" className="!m-0 flex items-center">
|
||||||
|
{t("always_requires_confirmation")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="notice"
|
||||||
|
value="notice"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="notice" className="!m-0 flex items-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
|
defaults="When booked with less than <time></time> notice"
|
||||||
|
components={{
|
||||||
|
time: (
|
||||||
|
<div className="mx-2 flex">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = Number(evt.target?.value);
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
unit:
|
||||||
|
requiresConfirmationSetup?.unit ??
|
||||||
|
defaultRequiresConfirmationSetup.unit,
|
||||||
|
time: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
||||||
|
}}
|
||||||
|
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
||||||
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = evt.target.value as UnitTypeLongPlural;
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
time:
|
||||||
|
requiresConfirmationSetup?.time ??
|
||||||
|
defaultRequiresConfirmationSetup.time,
|
||||||
|
unit: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
|
||||||
|
}}
|
||||||
|
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||||
|
defaultValue={
|
||||||
|
metadata?.requiresConfirmationThreshold?.unit ||
|
||||||
|
defaultRequiresConfirmationSetup.unit
|
||||||
|
}>
|
||||||
|
<option value="minutes">{t("minute_timeUnit")}</option>
|
||||||
|
<option value="hours">{t("hour_timeUnit")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,21 +4,20 @@ import { FormEvent, useRef, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
||||||
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserProfile {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
|
interface IUserProfileProps {
|
||||||
|
user: IOnboardingPageProps["user"];
|
||||||
|
}
|
||||||
|
|
||||||
const UserProfile = (props: IUserProfile) => {
|
const UserProfile = (props: IUserProfileProps) => {
|
||||||
const { user } = props;
|
const { user } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||||
@@ -100,7 +99,14 @@ const UserProfile = (props: IUserProfile) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||||
{user && <AvatarSSR user={user} alt="Profile picture" className="h-16 w-16" />}
|
{user && (
|
||||||
|
<Avatar
|
||||||
|
alt={user.username || "user avatar"}
|
||||||
|
gravatarFallbackMd5={user.emailMd5}
|
||||||
|
size="lg"
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserSettingsProps {
|
interface IUserSettingsProps {
|
||||||
user: User;
|
user: IOnboardingPageProps["user"];
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +24,9 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
control,
|
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: user?.name || "",
|
name: user?.name || "",
|
||||||
username: user?.username || "",
|
|
||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
@@ -41,39 +40,19 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
});
|
});
|
||||||
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
|
|
||||||
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (paymentRequired) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
timeZone: selectedTimeZone,
|
timeZone: selectedTimeZone,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentUsername, setCurrentUsername] = useState(user.username || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Username textfield */}
|
{/* Username textfield */}
|
||||||
<Controller
|
<UsernameAvailabilityField user={user} />
|
||||||
control={control}
|
|
||||||
name="username"
|
|
||||||
render={({ field: { value, ref, onChange } }) => (
|
|
||||||
<UsernameAvailability
|
|
||||||
readonly={true}
|
|
||||||
currentUsername={currentUsername}
|
|
||||||
setCurrentUsername={setCurrentUsername}
|
|
||||||
inputUsernameValue={value}
|
|
||||||
usernameRef={ref}
|
|
||||||
setInputUsernameValue={onChange}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
|
||||||
label: string;
|
|
||||||
value: EventTypeCustomInputType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
|
||||||
onCancel: () => void;
|
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const inputOptions: OptionTypeBase[] = [
|
|
||||||
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
|
|
||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
|
||||||
];
|
|
||||||
const { selectedCustomInput } = props;
|
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
|
||||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(props.onSubmit)}>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("input_type")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="type"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
id="type"
|
|
||||||
defaultValue={selectedInputOption}
|
|
||||||
options={inputOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
|
||||||
onChange={(option) => option && field.onChange(option.value)}
|
|
||||||
value={selectedInputOption}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("label")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="label"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.label}
|
|
||||||
{...register("label", { required: true })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
|
||||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("placeholder")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="placeholder"
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
|
||||||
{...register("placeholder")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="required"
|
|
||||||
type="checkbox"
|
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultChecked={selectedCustomInput?.required ?? true}
|
|
||||||
{...register("required")}
|
|
||||||
/>
|
|
||||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("is_required")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="eventTypeId"
|
|
||||||
value={selectedCustomInput?.eventTypeId || -1}
|
|
||||||
{...register("eventTypeId", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id"
|
|
||||||
value={selectedCustomInput?.id || -1}
|
|
||||||
{...register("id", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="mt-5 flex space-x-2 sm:mt-4">
|
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("save")}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Attendee, Booking, User } from "@prisma/client";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { JsonLd } from "react-schemaorg";
|
||||||
|
import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
|
||||||
|
|
||||||
|
type EventSchemaUser = Pick<User, "name" | "email">;
|
||||||
|
type EventSchemaAttendee = Pick<Attendee, "name" | "email">;
|
||||||
|
|
||||||
|
interface EventReservationSchemaInterface {
|
||||||
|
reservationId: Booking["uid"];
|
||||||
|
eventName: Booking["title"];
|
||||||
|
startTime: Booking["startTime"];
|
||||||
|
endTime: Booking["endTime"];
|
||||||
|
organizer: EventSchemaUser | null;
|
||||||
|
attendees: EventSchemaAttendee[];
|
||||||
|
location: Booking["location"];
|
||||||
|
description: Booking["description"];
|
||||||
|
status: Booking["status"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
|
||||||
|
reservationId,
|
||||||
|
eventName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
organizer,
|
||||||
|
attendees,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
const reservationStatus = useMemo<ReservationStatusType>(() => {
|
||||||
|
switch (status) {
|
||||||
|
case "ACCEPTED":
|
||||||
|
return "ReservationConfirmed";
|
||||||
|
case "REJECTED":
|
||||||
|
case "CANCELLED":
|
||||||
|
return "ReservationCancelled";
|
||||||
|
case "PENDING":
|
||||||
|
return "ReservationPending";
|
||||||
|
default:
|
||||||
|
return "ReservationHold";
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JsonLd<EventReservation>
|
||||||
|
item={{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "EventReservation",
|
||||||
|
reservationId,
|
||||||
|
reservationStatus,
|
||||||
|
reservationFor: {
|
||||||
|
"@type": "Event",
|
||||||
|
name: eventName,
|
||||||
|
startDate: startTime.toString(),
|
||||||
|
endDate: endTime.toString(),
|
||||||
|
organizer: organizer
|
||||||
|
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
|
||||||
|
: undefined,
|
||||||
|
attendee: attendees?.map(
|
||||||
|
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
|
||||||
|
),
|
||||||
|
location: location || undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventReservationSchema;
|
||||||
@@ -3,7 +3,7 @@ import { debounce, noop } from "lodash";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import { RefCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -24,9 +24,7 @@ import {
|
|||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
NORMAL = "NORMAL",
|
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
DOWNGRADE = "DOWNGRADE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
@@ -37,43 +35,21 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<
|
user: Pick<User, "username" | "metadata">;
|
||||||
User,
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "bio"
|
|
||||||
| "avatar"
|
|
||||||
| "timeZone"
|
|
||||||
| "weekStart"
|
|
||||||
| "hideBranding"
|
|
||||||
| "theme"
|
|
||||||
| "plan"
|
|
||||||
| "brandColor"
|
|
||||||
| "darkBrandColor"
|
|
||||||
| "timeFormat"
|
|
||||||
| "metadata"
|
|
||||||
>;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obtainNewUsernameChangeCondition = ({
|
const obtainNewUsernameChangeCondition = ({
|
||||||
userIsPremium,
|
userIsPremium,
|
||||||
isNewUsernamePremium,
|
isNewUsernamePremium,
|
||||||
stripeCustomer,
|
|
||||||
}: {
|
}: {
|
||||||
userIsPremium: boolean;
|
userIsPremium: boolean;
|
||||||
isNewUsernamePremium: boolean;
|
isNewUsernamePremium: boolean;
|
||||||
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) {
|
if (!userIsPremium && isNewUsernamePremium) {
|
||||||
return UsernameChangeStatusEnum.UPGRADE;
|
return UsernameChangeStatusEnum.UPGRADE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") {
|
|
||||||
return UsernameChangeStatusEnum.DOWNGRADE;
|
|
||||||
}
|
|
||||||
return UsernameChangeStatusEnum.NORMAL;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||||
@@ -106,7 +82,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
setIsInputUsernamePremium(data.premium);
|
setIsInputUsernamePremium(data.premium);
|
||||||
setUsernameIsAvailable(data.available);
|
setUsernameIsAvailable(data.available);
|
||||||
}, 150),
|
}, 150),
|
||||||
[]
|
[currentUsername]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -137,7 +113,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
||||||
const paymentRequired = !currentUsername && stripeCustomer?.isPremium && !stripeCustomer?.paidForPremium;
|
const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
|
||||||
|
|
||||||
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
||||||
userIsPremium: isCurrentUsernamePremium,
|
userIsPremium: isCurrentUsernamePremium,
|
||||||
@@ -194,7 +170,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveUsername = () => {
|
const saveUsername = () => {
|
||||||
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) {
|
if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
|
||||||
updateUsername.mutate({
|
updateUsername.mutate({
|
||||||
username: inputUsernameValue,
|
username: inputUsernameValue,
|
||||||
});
|
});
|
||||||
@@ -279,14 +255,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{paymentMsg}
|
{paymentMsg}
|
||||||
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
||||||
|
|
||||||
{usernameIsAvailable && (
|
|
||||||
<p className={classNames("mt-1 text-xs text-gray-900")}>
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
|
|
||||||
<>{t("premium_to_standard_username_description")}</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -295,13 +263,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-4 w-full px-4 pt-1">
|
<div className="mb-4 w-full px-4 pt-1">
|
||||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||||
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<p className="-mt-4 mb-4 text-sm text-gray-800">
|
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
|
|
||||||
t("change_username_standard_to_premium")}
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
|
|
||||||
t("change_username_premium_to_standard")}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
||||||
@@ -323,8 +286,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
||||||
{/* redirect to checkout */}
|
{/* redirect to checkout */}
|
||||||
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE ||
|
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
@@ -336,7 +298,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Normal save */}
|
{/* Normal save */}
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<Input
|
<Input
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
|
value={inputUsernameValue}
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
@@ -133,7 +134,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||||
: ""
|
: ""
|
||||||
)}
|
)}
|
||||||
defaultValue={currentUsername}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setInputUsernameValue(event.target.value);
|
setInputUsernameValue(event.target.value);
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
|
import { User } from "@calcom/prisma/client";
|
||||||
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
import { PremiumTextfield } from "./PremiumTextfield";
|
import { PremiumTextfield } from "./PremiumTextfield";
|
||||||
import { UsernameTextfield } from "./UsernameTextfield";
|
import { UsernameTextfield } from "./UsernameTextfield";
|
||||||
|
|
||||||
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
||||||
|
|
||||||
|
interface UsernameAvailabilityFieldProps {
|
||||||
|
onSuccessMutation?: () => void;
|
||||||
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
|
user: Pick<User, "username" | "metadata">;
|
||||||
|
}
|
||||||
|
export const UsernameAvailabilityField = ({
|
||||||
|
onSuccessMutation,
|
||||||
|
onErrorMutation,
|
||||||
|
user,
|
||||||
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
|
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
|
||||||
|
const formMethods = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
username: currentUsername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={formMethods.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field: { ref, onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<UsernameAvailability
|
||||||
|
currentUsername={currentUsername}
|
||||||
|
setCurrentUsername={setCurrentUsername}
|
||||||
|
inputUsernameValue={value}
|
||||||
|
usernameRef={ref}
|
||||||
|
setInputUsernameValue={onChange}
|
||||||
|
onSuccessMutation={onSuccessMutation}
|
||||||
|
onErrorMutation={onErrorMutation}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { UserPlan } from "@prisma/client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: It should be exposed at a single place.
|
|
||||||
*/
|
|
||||||
export function useExposePlanGlobally(plan: UserPlan) {
|
|
||||||
// Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// This variable is used by embed-iframe to determine if we should allow UI configuration
|
|
||||||
window.CalComPlan = plan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { User } from "@prisma/client";
|
export function isBrandingHidden(hideBrandingSetting: boolean, belongsToActiveTeam: boolean) {
|
||||||
|
return belongsToActiveTeam && hideBrandingSetting;
|
||||||
export function isBrandingHidden<TUser extends Pick<User, "hideBranding" | "plan">>(user: TUser) {
|
|
||||||
return user.hideBranding && user.plan !== "FREE";
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,29 @@ export type EmbedProps = {
|
|||||||
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||||
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||||
const ssrResponse = await getServerSideProps(context);
|
const ssrResponse = await getServerSideProps(context);
|
||||||
|
const embed = context.query.embed;
|
||||||
|
|
||||||
|
if ("redirect" in ssrResponse) {
|
||||||
|
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
||||||
|
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
|
||||||
|
|
||||||
|
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||||
|
const newDestinationUrl =
|
||||||
|
destinationUrlObj.pathname +
|
||||||
|
"/embed?" +
|
||||||
|
destinationUrlObj.searchParams.toString() +
|
||||||
|
"&embed=" +
|
||||||
|
embed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ssrResponse,
|
||||||
|
redirect: {
|
||||||
|
...ssrResponse.redirect,
|
||||||
|
destination: newDestinationUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!("props" in ssrResponse)) {
|
if (!("props" in ssrResponse)) {
|
||||||
return ssrResponse;
|
return ssrResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,11 @@ const nextConfig = {
|
|||||||
destination: "/404",
|
destination: "/404",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/booking/direct/:action/:email/:bookingUid/:oldToken",
|
||||||
|
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.3.4",
|
"version": "2.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.0.19",
|
"@vercel/og": "^0.0.21",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -107,19 +107,21 @@
|
|||||||
"react-live-chat-loader": "^2.7.3",
|
"react-live-chat-loader": "^2.7.3",
|
||||||
"react-multi-email": "^0.5.3",
|
"react-multi-email": "^0.5.3",
|
||||||
"react-phone-number-input": "^3.2.7",
|
"react-phone-number-input": "^3.2.7",
|
||||||
|
"react-schemaorg": "^2.0.0",
|
||||||
"react-select": "^5.4.0",
|
"react-select": "^5.4.0",
|
||||||
"react-timezone-select": "^1.3.2",
|
"react-timezone-select": "^1.3.2",
|
||||||
"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",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web3": "^1.7.5",
|
"web3": "^1.7.5",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
|
|||||||
+33
-14
@@ -1,30 +1,49 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { Button } from "@calcom/ui";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
export default function Error500() {
|
export default function Error500() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen bg-gray-100">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Something unexpected occurred | {APP_NAME}</title>
|
<title>Something unexpected occurred | {APP_NAME}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="m-auto text-center">
|
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
|
||||||
<h1 className="font-cal text-[250px] text-gray-900">
|
<h1 className="font-cal text-6xl text-black">500</h1>
|
||||||
5
|
<h2 className="mt-6 text-2xl font-medium text-black">It's not you, it's us.</h2>
|
||||||
{
|
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src="/error.svg" className="-mt-10 inline w-60" alt="0" />
|
|
||||||
}
|
|
||||||
0
|
|
||||||
</h1>
|
|
||||||
<h2 className="mb-2 -mt-16 text-3xl text-gray-600">It's not you, it's us.</h2>
|
|
||||||
<p className="mb-4 max-w-2xl text-gray-500">
|
|
||||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||||
away for you.
|
away for you.
|
||||||
</p>
|
</p>
|
||||||
<Button href={`${WEBSITE_URL}/support`}>Contact support</Button>
|
{router.query.error && (
|
||||||
|
<div className="mb-8 flex flex-col">
|
||||||
|
<p className="mb-4 max-w-2xl text-sm text-gray-600">
|
||||||
|
Please provide the following text when contacting support to better help you:
|
||||||
|
</p>
|
||||||
|
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
|
||||||
|
{router.query.error}
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||||
|
StartIcon={Icon.FiCopy}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(router.query.error as string);
|
||||||
|
showToast("Link copied!", "success");
|
||||||
|
}}>
|
||||||
|
{t("copy")}
|
||||||
|
</Button>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import defaultEvents, {
|
import defaultEvents, {
|
||||||
@@ -26,9 +27,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
|
import { BadgeCheckIcon, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{eventTypes.map((type, index) => (
|
{eventTypes.map((type, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="dark:bg-darkgray-100 dark:border-darkgray-200 group relative rounded-sm border border-neutral-200 bg-white dark:hover:border-neutral-600">
|
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
|
||||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
||||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
||||||
@@ -69,7 +69,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 self-center">
|
<div className="mt-1 self-center">
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
border="border-2 border-white"
|
border="border-2 border-white dark:border-darkgray-100"
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
className="flex flex-shrink-0"
|
className="flex flex-shrink-0"
|
||||||
size={10}
|
size={10}
|
||||||
@@ -92,7 +92,6 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const query = { ...router.query };
|
const query = { ...router.query };
|
||||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||||
useExposePlanGlobally("PRO");
|
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
@@ -272,7 +271,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { JSONObject } from "superjson/dist/types";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { User } from "@calcom/prisma/client";
|
import { User } from "@calcom/prisma/client";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -71,7 +72,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
away: true,
|
away: true,
|
||||||
plan: true,
|
|
||||||
name: true,
|
name: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
@@ -110,6 +110,11 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
teams: {
|
||||||
|
include: {
|
||||||
|
team: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,10 +126,10 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
"strikethrough",
|
"strikethrough",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!user || !user.eventTypes) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
const [eventType]: (typeof user.eventTypes[number] & {
|
const [eventType]: (typeof user.eventTypes[number] & {
|
||||||
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
|
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
|
||||||
})[] = [
|
})[] = [
|
||||||
{
|
{
|
||||||
...user.eventTypes[0],
|
...user.eventTypes[0],
|
||||||
@@ -133,7 +138,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -150,6 +154,14 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
|
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
|
||||||
});
|
});
|
||||||
|
// Check if the user you are logging into has any active teams
|
||||||
|
const hasActiveTeam =
|
||||||
|
user.teams.filter((m) => {
|
||||||
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||||
|
return false;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@@ -167,6 +179,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
@@ -212,7 +225,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +244,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -262,6 +273,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma";
|
import { bookEventTypeSelect } from "@calcom/prisma";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import getBooking, { GetBookingType } from "@lib/getBooking";
|
import getBooking, { GetBookingType } from "@lib/getBooking";
|
||||||
@@ -127,6 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||||
schedulingType: null,
|
schedulingType: null,
|
||||||
|
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
||||||
users: users.map((u) => ({
|
users: users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name: u.name,
|
name: u.name,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import Script from "next/script";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
@@ -36,8 +37,11 @@ function MyApp(props: AppProps) {
|
|||||||
<AppProviders {...props}>
|
<AppProviders {...props}>
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
<I18nLanguageHandler />
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
{getLayout(
|
{getLayout(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
|
||||||
function toRunBeforeReactOnClient() {
|
function toRunBeforeReactOnClient() {
|
||||||
const calEmbedMode =
|
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
|
||||||
location.search.includes("embed=") ||
|
/* Iframe Name */
|
||||||
/* Iframe Name */
|
window.name.includes("cal-embed");
|
||||||
window.name.includes("cal-embed");
|
|
||||||
|
|
||||||
window.isEmbed = () => {
|
window.isEmbed = () => {
|
||||||
// Once an embed mode always an embed mode
|
// Once an embed mode always an embed mode
|
||||||
@@ -70,7 +70,9 @@ class MyDocument extends Document<Props> {
|
|||||||
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
||||||
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
||||||
*/}
|
*/}
|
||||||
<script
|
<Script
|
||||||
|
id="run-before-client"
|
||||||
|
strategy="beforeInteractive"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
|||||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||||
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
import rateLimit from "@calcom/lib/rateLimit";
|
import rateLimit from "@calcom/lib/rateLimit";
|
||||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||||
import { randomString } from "@lib/random";
|
import { randomString } from "@lib/random";
|
||||||
@@ -63,6 +64,11 @@ const providers: Provider[] = [
|
|||||||
password: true,
|
password: true,
|
||||||
twoFactorEnabled: true,
|
twoFactorEnabled: true,
|
||||||
twoFactorSecret: true,
|
twoFactorSecret: true,
|
||||||
|
teams: {
|
||||||
|
include: {
|
||||||
|
team: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,6 +122,14 @@ const providers: Provider[] = [
|
|||||||
intervalInMs: 60 * 1000, // 1 minute
|
intervalInMs: 60 * 1000, // 1 minute
|
||||||
});
|
});
|
||||||
await limiter.check(10, user.email); // 10 requests per minute
|
await limiter.check(10, user.email); // 10 requests per minute
|
||||||
|
// Check if the user you are logging into has any active teams
|
||||||
|
const hasActiveTeams =
|
||||||
|
user.teams.filter((m) => {
|
||||||
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||||
|
return false;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
// authentication success- but does it meet the minimum password requirements?
|
// authentication success- but does it meet the minimum password requirements?
|
||||||
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
||||||
@@ -125,6 +139,7 @@ const providers: Provider[] = [
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: "USER",
|
role: "USER",
|
||||||
|
belongsToActiveTeam: hasActiveTeams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +149,7 @@ const providers: Provider[] = [
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
belongsToActiveTeam: hasActiveTeams,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -272,6 +288,7 @@ export default NextAuth({
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
impersonatedByUID: user?.impersonatedByUID,
|
impersonatedByUID: user?.impersonatedByUID,
|
||||||
|
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +324,7 @@ export default NextAuth({
|
|||||||
email: existingUser.email,
|
email: existingUser.email,
|
||||||
role: existingUser.role,
|
role: existingUser.role,
|
||||||
impersonatedByUID: token.impersonatedByUID as number,
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +342,7 @@ export default NextAuth({
|
|||||||
username: token.username as string,
|
username: token.username as string,
|
||||||
role: token.role as UserPermissionRole,
|
role: token.role as UserPermissionRole,
|
||||||
impersonatedByUID: token.impersonatedByUID as number,
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return calendsoSession;
|
return calendsoSession;
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ async function handler(req: NextApiRequest) {
|
|||||||
name: parsedQuery.data.full_name,
|
name: parsedQuery.data.full_name,
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
locale: "en", // TODO: We should revisit this
|
locale: "en", // TODO: We should revisit this
|
||||||
plan: "PRO",
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
identityProvider: IdentityProvider.CAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||||
if (process.env.CRON_API_KEY !== apiKey) {
|
if (process.env.CRON_API_KEY !== apiKey) {
|
||||||
@@ -18,35 +13,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* We should add and extra check for non-paying customers in Stripe so we can
|
* Remove this endpoint
|
||||||
* downgrade them here.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
await prisma.user.updateMany({
|
|
||||||
data: {
|
|
||||||
plan: "FREE",
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
plan: "TRIAL",
|
|
||||||
OR: [
|
|
||||||
/**
|
|
||||||
* If the user doesn't have a trial end date,
|
|
||||||
* use the default 14 day trial from creation.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
createdDate: {
|
|
||||||
lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
|
|
||||||
},
|
|
||||||
trialEndsAt: null,
|
|
||||||
},
|
|
||||||
/** If it does, then honor the trial end date. */
|
|
||||||
{
|
|
||||||
trialEndsAt: {
|
|
||||||
lt: dayjs().toDate(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
title: "30min between Pro Example and pro@example.com",
|
title: "30min between Pro Example and pro@example.com",
|
||||||
description: null,
|
description: null,
|
||||||
additionalNotes: "asdasdas",
|
additionalNotes: "asdasdas",
|
||||||
customInputs: {
|
|
||||||
"Custom input 01": "sadasdasdsadasd",
|
|
||||||
"Custom input 02": "asdasdasd",
|
|
||||||
},
|
|
||||||
startTime: "2022-06-03T09:00:00-06:00",
|
startTime: "2022-06-03T09:00:00-06:00",
|
||||||
endTime: "2022-06-03T09:30:00-06:00",
|
endTime: "2022-06-03T09:30:00-06:00",
|
||||||
organizer: {
|
organizer: {
|
||||||
@@ -32,26 +28,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
timeZone: "America/Chihuahua",
|
timeZone: "America/Chihuahua",
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
email: "pro@example.com",
|
|
||||||
name: "pro@example.com",
|
|
||||||
timeZone: "America/Chihuahua",
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: "pro@example.com",
|
|
||||||
name: "pro@example.com",
|
|
||||||
timeZone: "America/Chihuahua",
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
location: "Zoom video",
|
location: "Zoom video",
|
||||||
destinationCalendar: null,
|
destinationCalendar: null,
|
||||||
hideCalendarNotes: false,
|
hideCalendarNotes: false,
|
||||||
uid: "bwPWLpjYrx4rZf6MCZdKgE",
|
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
cancellationReason: "It got late",
|
|
||||||
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
|
|
||||||
recurringEvent: null,
|
recurringEvent: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,7 +42,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("AttendeeScheduledEmail", {
|
renderEmail("OrganizerRequestEmail", {
|
||||||
attendee: evt.attendees[0],
|
attendee: evt.attendees[0],
|
||||||
calEvent: evt,
|
calEvent: evt,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
||||||
|
|
||||||
|
enum DirectAction {
|
||||||
|
ACCEPT = "accept",
|
||||||
|
REJECT = "reject",
|
||||||
|
}
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
action: z.nativeEnum(DirectAction),
|
||||||
|
token: z.string(),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSchema = z.object({
|
||||||
|
bookingUid: z.string(),
|
||||||
|
userId: z.number().int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
|
const { action, token, reason } = querySchema.parse(req.query);
|
||||||
|
const { bookingUid, userId } = decryptedSchema.parse(
|
||||||
|
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||||
|
);
|
||||||
|
|
||||||
|
/** We shape the session as required by tRPC router */
|
||||||
|
async function sessionGetter() {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username: "" /* Not used in this context */,
|
||||||
|
role: UserPermissionRole.USER,
|
||||||
|
},
|
||||||
|
hasValidLicense: true,
|
||||||
|
expires: "" /* Not used in this context */,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findUniqueOrThrow({
|
||||||
|
where: { uid: bookingUid },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
|
const ctx = await createContext({ req, res }, sessionGetter);
|
||||||
|
const caller = viewerRouter.createCaller(ctx);
|
||||||
|
await caller.bookings.confirm({
|
||||||
|
bookingId: booking.id,
|
||||||
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
|
confirmed: action === DirectAction.ACCEPT,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
let message = "Error confirming booking";
|
||||||
|
if (e instanceof TRPCError) message = e.message;
|
||||||
|
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(`/booking/${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(handler);
|
||||||
@@ -53,6 +53,8 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
interFontMedium,
|
interFontMedium,
|
||||||
]);
|
]);
|
||||||
const ogConfig = {
|
const ogConfig = {
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
fonts: [
|
fonts: [
|
||||||
{ name: "inter", data: interFontData, weight: 400 },
|
{ name: "inter", data: interFontData, weight: 400 },
|
||||||
{ name: "inter", data: interFontMediumData, weight: 500 },
|
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||||
@@ -71,7 +73,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
meetingImage: searchParams.get("meetingImage"),
|
meetingImage: searchParams.get("meetingImage"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
return new ImageResponse(
|
const img = new ImageResponse(
|
||||||
(
|
(
|
||||||
<Meeting
|
<Meeting
|
||||||
title={title}
|
title={title}
|
||||||
@@ -80,7 +82,9 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
ogConfig
|
ogConfig
|
||||||
);
|
) as { body: Buffer };
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
case "app": {
|
case "app": {
|
||||||
const { name, description, slug } = appSchema.parse({
|
const { name, description, slug } = appSchema.parse({
|
||||||
@@ -89,7 +93,11 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
slug: searchParams.get("slug"),
|
slug: searchParams.get("slug"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
|
||||||
|
body: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "generic": {
|
case "generic": {
|
||||||
@@ -99,7 +107,11 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||||
|
body: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
select: {
|
select: {
|
||||||
username: true,
|
username: true,
|
||||||
id: true,
|
id: true,
|
||||||
plan: true,
|
|
||||||
createdDate: true,
|
createdDate: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -81,7 +80,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||||
: "No info"
|
: "No info"
|
||||||
}</li>
|
}</li>
|
||||||
<li><b>Plan:</b> ${user.plan}</li>
|
|
||||||
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { MembershipRole } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
import slugify from "@lib/slugify";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownerUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
createdDate: true,
|
|
||||||
name: true,
|
|
||||||
plan: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
|
||||||
const slug = slugify(req.body.name);
|
|
||||||
|
|
||||||
const nameCollisions = await prisma.team.count({
|
|
||||||
where: {
|
|
||||||
OR: [{ name: req.body.name }, { slug: slug }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nameCollisions > 0) {
|
|
||||||
return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already taken." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTeam = await prisma.team.create({
|
|
||||||
data: {
|
|
||||||
name: req.body.name,
|
|
||||||
slug: slug,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.membership.create({
|
|
||||||
data: {
|
|
||||||
teamId: createTeam.id,
|
|
||||||
userId: session.user.id,
|
|
||||||
role: MembershipRole.OWNER,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync Services: Close.com
|
|
||||||
closeComUpsertTeamUser(createTeam, ownerUser, MembershipRole.OWNER);
|
|
||||||
|
|
||||||
return res.status(201).json({ message: "Team created" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(404).json({ message: "Team not found" });
|
|
||||||
}
|
|
||||||
@@ -1,86 +1,72 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { defaultAvatarSrc } from "@lib/profile";
|
import { defaultAvatarSrc } from "@lib/profile";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
const querySchema = z
|
||||||
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
|
.object({
|
||||||
const username = req.query.username as string;
|
username: z.string(),
|
||||||
const teamname = req.query.teamname as string;
|
teamname: z.string(),
|
||||||
let identity;
|
})
|
||||||
let linksToThisRoute = false;
|
.partial();
|
||||||
|
|
||||||
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
|
const { username, teamname } = querySchema.parse(req.query);
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: { username },
|
||||||
username: username,
|
select: { avatar: true, email: true },
|
||||||
},
|
|
||||||
select: {
|
|
||||||
avatar: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
identity = {
|
return {
|
||||||
name: username,
|
name: username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
avatar: user?.avatar,
|
avatar: user?.avatar,
|
||||||
};
|
};
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
|
|
||||||
} else if (teamname) {
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: {
|
|
||||||
slug: teamname,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
logo: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
identity = {
|
|
||||||
name: teamname,
|
|
||||||
shouldDefaultBeNameBased: true,
|
|
||||||
avatar: team?.logo,
|
|
||||||
};
|
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
|
|
||||||
}
|
}
|
||||||
|
if (teamname) {
|
||||||
const emailMd5 = crypto
|
const team = await prisma.team.findUnique({
|
||||||
.createHash("md5")
|
where: { slug: teamname },
|
||||||
.update((identity?.email as string) || "guest@example.com")
|
select: { logo: true },
|
||||||
.digest("hex");
|
|
||||||
const img = identity?.avatar;
|
|
||||||
// If image isn't set or links to this route itself, use default avatar
|
|
||||||
if (!img || linksToThisRoute) {
|
|
||||||
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
|
||||||
if (identity?.shouldDefaultBeNameBased) {
|
|
||||||
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
|
||||||
}
|
|
||||||
res.writeHead(302, {
|
|
||||||
Location: defaultSrc,
|
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
res.end();
|
name: teamname,
|
||||||
} else if (!img.includes("data:image")) {
|
email: null,
|
||||||
res.writeHead(302, {
|
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
|
||||||
Location: img,
|
};
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
const decoded = img
|
|
||||||
.toString()
|
|
||||||
.replace("data:image/png;base64,", "")
|
|
||||||
.replace("data:image/jpeg;base64,", "");
|
|
||||||
const imageResp = Buffer.from(decoded, "base64");
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "image/png",
|
|
||||||
"Content-Length": imageResp.length,
|
|
||||||
});
|
|
||||||
res.end(imageResp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const identity = await getIdentityData(req);
|
||||||
|
const img = identity?.avatar;
|
||||||
|
// If image isn't set or links to this route itself, use default avatar
|
||||||
|
if (!img) {
|
||||||
|
res.writeHead(302, {
|
||||||
|
Location: defaultAvatarSrc({
|
||||||
|
md5: crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(identity?.email || "guest@example.com")
|
||||||
|
.digest("hex"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!img.includes("data:image")) {
|
||||||
|
res.writeHead(302, { Location: img });
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||||
|
const imageResp = Buffer.from(decoded, "base64");
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Content-Length": imageResp.length,
|
||||||
|
});
|
||||||
|
res.end(imageResp);
|
||||||
|
}
|
||||||
@@ -47,13 +47,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const paths = Object.keys(AppCategories);
|
||||||
const paths = appStore.reduce((categories, app) => {
|
|
||||||
if (!categories.includes(app.category)) {
|
|
||||||
categories.push(app.category);
|
|
||||||
}
|
|
||||||
return categories;
|
|
||||||
}, [] as string[]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: paths.map((category) => ({ params: { category } })),
|
paths: paths.map((category) => ({ params: { category } })),
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const categories = appStore.reduce((c, app) => {
|
const categories = appStore.reduce((c, app) => {
|
||||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
for (const category of app.categories) {
|
||||||
|
c[category] = c[category] ? c[category] + 1 : 1;
|
||||||
|
}
|
||||||
return c;
|
return c;
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { InferGetStaticPropsType, NextPageContext } from "next";
|
|
||||||
import { ChangeEventHandler, useState } from "react";
|
import { ChangeEventHandler, useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
@@ -7,6 +6,7 @@ import { classNames } from "@calcom/lib";
|
|||||||
import { getSession } from "@calcom/lib/auth";
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { AppCategories } from "@calcom/prisma/client";
|
import type { AppCategories } from "@calcom/prisma/client";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
@@ -31,7 +31,7 @@ function AppsSearch({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
@@ -42,19 +42,24 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
|||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
actions={(className) => (
|
actions={(className) => (
|
||||||
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
||||||
)}>
|
)}
|
||||||
|
emptyStore={!appStore.length}>
|
||||||
{!searchText && (
|
{!searchText && (
|
||||||
<>
|
<>
|
||||||
<AppStoreCategories categories={categories} />
|
<AppStoreCategories categories={categories} />
|
||||||
<TrendingAppsSlider items={appStore} />
|
<TrendingAppsSlider items={appStore} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<AllApps apps={appStore} searchText={searchText} />
|
<AllApps
|
||||||
|
apps={appStore}
|
||||||
|
searchText={searchText}
|
||||||
|
categories={categories.map((category) => category.name)}
|
||||||
|
/>
|
||||||
</AppsLayout>
|
</AppsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: NextPageContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const ssg = await ssgInit(context);
|
const ssg = await ssgInit(context);
|
||||||
|
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
@@ -77,7 +82,6 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: ssg.dehydrate(),
|
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
name: name as AppCategories,
|
name: name as AppCategories,
|
||||||
@@ -87,6 +91,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -7,12 +7,13 @@ import { useState } from "react";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { FaGoogle } from "react-icons/fa";
|
import { FaGoogle } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Alert, Button, EmailField, Icon, PasswordField, SAMLLogin } from "@calcom/ui";
|
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
|
||||||
|
|
||||||
import { ErrorCode, getSession } from "@lib/auth";
|
import { ErrorCode, getSession } from "@lib/auth";
|
||||||
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
|
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||||
|
import StepDone from "./steps/StepDone";
|
||||||
|
|
||||||
|
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||||
|
const shouldDisable = props.userCount !== 0;
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: t("administrator_user"),
|
||||||
|
description: t("lets_create_first_administrator_user"),
|
||||||
|
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||||
|
isLoading: isLoadingStep1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("enable_apps"),
|
||||||
|
description: t("enable_apps_description"),
|
||||||
|
content: <AdminAppsList baseURL="/auth/setup?step=2" />,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex items-center bg-gray-100 print:h-full">
|
||||||
|
<WizardForm
|
||||||
|
href="/auth/setup"
|
||||||
|
steps={steps}
|
||||||
|
nextLabel={t("next_step_text")}
|
||||||
|
finishLabel={t("finish")}
|
||||||
|
prevLabel={t("prev_step")}
|
||||||
|
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
const { req } = context;
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/404`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
userCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
+12
-59
@@ -1,34 +1,14 @@
|
|||||||
import { CheckIcon } from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|
||||||
import { EmailField, Label, PasswordField, TextField, WizardForm } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const StepDone = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
|
||||||
<CheckIcon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -83,13 +63,19 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
router.replace(`/auth/login?email=${data.email_address.toLowerCase()}`);
|
await signIn("credentials", {
|
||||||
|
redirect: false,
|
||||||
|
callbackUrl: "/",
|
||||||
|
email: data.email_address.toLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
router.replace("/auth/setup");
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
const longWebsiteUrl = process.env.NEXT_PUBLIC_WEBSITE_URL.length > 30;
|
const longWebsiteUrl = WEBSITE_URL.length > 30;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
@@ -201,37 +187,4 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
export default SetupFormStep1;
|
||||||
const { t } = useLocale();
|
|
||||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: t("administrator_user"),
|
|
||||||
description: t("lets_create_first_administrator_user"),
|
|
||||||
content: props.userCount !== 0 ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
|
||||||
enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps
|
|
||||||
isLoading: isLoadingStep1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="flex h-screen items-center bg-gray-100 print:h-full">
|
|
||||||
<WizardForm href="/auth/setup" steps={steps} containerClassname="max-w-sm" />
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
userCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
const StepDone = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="wizard-step-1"
|
||||||
|
name="wizard-step-1"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
|
}}>
|
||||||
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
|
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-[420px] text-center">
|
||||||
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepDone;
|
||||||
@@ -3,7 +3,7 @@ import { signIn } from "next-auth/react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||||
import {
|
import {
|
||||||
hostedCal,
|
hostedCal,
|
||||||
@@ -161,7 +161,7 @@ const getStripePremiumUsernameUrl = async ({
|
|||||||
customer: customer.id,
|
customer: customer.id,
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
price: getPremiumPlanPrice(),
|
price: getPremiumMonthlyPlanPriceId(),
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { GetStaticPaths, GetStaticProps } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useEffect } from "react";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
import { availabilityAsString } from "@calcom/lib/availability";
|
||||||
|
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||||
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 type { Schedule as ScheduleType } from "@calcom/types/schedule";
|
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
SkeletonText,
|
SkeletonText,
|
||||||
Switch,
|
Switch,
|
||||||
TimezoneSelect,
|
TimezoneSelect,
|
||||||
|
Tooltip,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
schedule: stringOrNumber,
|
schedule: stringOrNumber,
|
||||||
});
|
});
|
||||||
@@ -36,31 +40,59 @@ const querySchema = z.object({
|
|||||||
type AvailabilityFormValues = {
|
type AvailabilityFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
schedule: ScheduleType;
|
schedule: ScheduleType;
|
||||||
|
dateOverrides: { ranges: TimeRange[] }[];
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||||
|
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||||
|
name: "dateOverrides",
|
||||||
|
});
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="font-medium leading-6 text-gray-900">
|
||||||
|
{t("date_overrides")}{" "}
|
||||||
|
<Tooltip content={t("date_overrides_info")}>
|
||||||
|
<span className="inline-block">
|
||||||
|
<Icon.FiInfo />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</h3>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
||||||
|
<div className="mt-1 space-y-2">
|
||||||
|
<DateOverrideList
|
||||||
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
|
remove={remove}
|
||||||
|
update={update}
|
||||||
|
items={fields}
|
||||||
|
workingHours={workingHours}
|
||||||
|
/>
|
||||||
|
<DateOverrideInputDialog
|
||||||
|
workingHours={workingHours}
|
||||||
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
|
onChange={(ranges) => append({ ranges })}
|
||||||
|
Trigger={
|
||||||
|
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
|
||||||
|
Add an override
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Availability({ schedule }: { schedule: number }) {
|
export default function Availability({ schedule }: { schedule: number }) {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const me = useMeQuery();
|
const me = useMeQuery();
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
||||||
|
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
|
||||||
const form = useForm<AvailabilityFormValues>();
|
const form = useForm<AvailabilityFormValues>({ defaultValues });
|
||||||
const { control, reset } = form;
|
const { control } = form;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && data) {
|
|
||||||
reset({
|
|
||||||
name: data?.schedule?.name,
|
|
||||||
schedule: data.availability,
|
|
||||||
timeZone: data.timeZone,
|
|
||||||
isDefault: data.isDefault,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, isLoading, reset]);
|
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
||||||
if (prevDefaultId && currentDefaultId) {
|
if (prevDefaultId && currentDefaultId) {
|
||||||
@@ -71,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
|
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
|
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
|
||||||
utils.viewer.availability.list.invalidate();
|
utils.viewer.availability.list.invalidate();
|
||||||
showToast(
|
showToast(
|
||||||
t("availability_updated_successfully", {
|
t("availability_updated_successfully", {
|
||||||
@@ -91,22 +123,26 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/availability"
|
backPath="/availability"
|
||||||
title={data?.schedule.name && data.schedule.name + " | " + t("availability")}
|
title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
|
||||||
heading={
|
heading={
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => <EditableHeading isReady={!isLoading} {...field} />}
|
render={({ field }) => (
|
||||||
|
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
data ? (
|
data ? (
|
||||||
data.schedule.availability.map((availability) => (
|
data.schedule.availability
|
||||||
<span key={availability.id}>
|
.filter((availability) => !!availability.days.length)
|
||||||
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
|
.map((availability) => (
|
||||||
<br />
|
<span key={availability.id}>
|
||||||
</span>
|
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
|
||||||
))
|
<br />
|
||||||
|
</span>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText className="h-4 w-48" />
|
<SkeletonText className="h-4 w-48" />
|
||||||
)
|
)
|
||||||
@@ -138,93 +174,88 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="flex items-baseline sm:mt-0">
|
<div className="w-full">
|
||||||
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
|
<Form
|
||||||
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
|
form={form}
|
||||||
<div className="w-full">
|
id="availability-form"
|
||||||
<Form
|
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||||
form={form}
|
updateMutation.mutate({
|
||||||
id="availability-form"
|
scheduleId: schedule,
|
||||||
handleSubmit={async (values) => {
|
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||||
updateMutation.mutate({
|
...values,
|
||||||
scheduleId: schedule,
|
});
|
||||||
...values,
|
}}
|
||||||
});
|
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||||
}}
|
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
||||||
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
<div className=" py-5 sm:p-6">
|
||||||
<div className="flex-1">
|
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
|
||||||
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
|
{t("change_start_end")}
|
||||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
</h3>
|
||||||
{t("change_start_end")}
|
{typeof me.data?.weekStart === "string" && (
|
||||||
</h3>
|
<Schedule
|
||||||
{typeof me.data?.weekStart === "string" && (
|
control={control}
|
||||||
<Schedule
|
name="schedule"
|
||||||
control={control}
|
weekStart={
|
||||||
name="schedule"
|
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||||
weekStart={
|
me.data?.weekStart
|
||||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||||
me.data?.weekStart
|
}
|
||||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
/>
|
||||||
}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||||
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
</div>
|
||||||
<div>
|
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||||
{t("timezone")}
|
<div>
|
||||||
</label>
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
<Controller
|
{t("timezone")}
|
||||||
name="timeZone"
|
</label>
|
||||||
render={({ field: { onChange, value } }) =>
|
<Controller
|
||||||
value ? (
|
name="timeZone"
|
||||||
<TimezoneSelect
|
render={({ field: { onChange, value } }) =>
|
||||||
value={value}
|
value ? (
|
||||||
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
<TimezoneSelect
|
||||||
onChange={(timezone) => onChange(timezone.value)}
|
value={value}
|
||||||
/>
|
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
||||||
) : (
|
onChange={(timezone) => onChange(timezone.value)}
|
||||||
<SelectSkeletonLoader className="w-72" />
|
/>
|
||||||
)
|
) : (
|
||||||
}
|
<SelectSkeletonLoader className="w-72" />
|
||||||
/>
|
)
|
||||||
</div>
|
}
|
||||||
<hr className="my-8" />
|
/>
|
||||||
<div className="rounded-md">
|
</div>
|
||||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
<hr className="my-8" />
|
||||||
<div className="mt-3 flex">
|
<div className="rounded-md">
|
||||||
<Button href="/availability/troubleshoot" color="secondary">
|
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||||
{t("launch_troubleshooter")}
|
<div className="mt-3 flex">
|
||||||
</Button>
|
<Button href="/availability/troubleshoot" color="secondary">
|
||||||
</div>
|
{t("launch_troubleshooter")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</div>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (ctx) => {
|
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
|
const ssr = await ssrInit(ctx);
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
if (!params.success) return { notFound: true };
|
||||||
|
|
||||||
|
const scheduleId = params.data.schedule;
|
||||||
|
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
|
||||||
|
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
schedule: params.data.schedule,
|
schedule: scheduleId,
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: "blocking",
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -10,6 +11,8 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
|
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
@@ -45,6 +48,24 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
|
onSuccess: async ({ schedule }) => {
|
||||||
|
await utils.viewer.availability.list.invalidate();
|
||||||
|
showToast(
|
||||||
|
t("availability_updated_successfully", {
|
||||||
|
scheduleName: schedule.name,
|
||||||
|
}),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
if (err instanceof HttpError) {
|
||||||
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Adds smooth delete button - item fades and old item slides into place
|
// Adds smooth delete button - item fades and old item slides into place
|
||||||
|
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
@@ -71,6 +92,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
}}
|
}}
|
||||||
key={schedule.id}
|
key={schedule.id}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
|
updateDefault={updateMutation.mutate}
|
||||||
deleteFunction={deleteMutation.mutate}
|
deleteFunction={deleteMutation.mutate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -93,3 +115,13 @@ export default function AvailabilityPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Shell, SkeletonText } from "@calcom/ui";
|
import { Shell, SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
type User = RouterOutputs["viewer"]["me"];
|
type User = RouterOutputs["viewer"]["me"];
|
||||||
|
|
||||||
export interface IBusySlot {
|
export interface IBusySlot {
|
||||||
@@ -16,7 +16,9 @@ export interface IBusySlot {
|
|||||||
|
|
||||||
const AvailabilityView = ({ user }: { user: User }) => {
|
const AvailabilityView = ({ user }: { user: User }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
||||||
|
const selectedDate = dayjs(date);
|
||||||
|
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,17 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const overrides =
|
||||||
|
data?.dateOverrides.reduce((acc, override) => {
|
||||||
|
if (
|
||||||
|
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
|
||||||
|
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
|
||||||
|
)
|
||||||
|
return acc;
|
||||||
|
acc.push({ ...override, source: "Date override" });
|
||||||
|
return acc;
|
||||||
|
}, [] as IBusySlot[]) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
@@ -37,9 +50,9 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="inline h-8 border-none p-0"
|
className="inline h-8 border-none p-0"
|
||||||
defaultValue={selectedDate.format("YYYY-MM-DD")}
|
defaultValue={formattedSelectedDate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value) setSelectedDate(dayjs(e.target.value));
|
if (e.target.value) setSelectedDate(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
||||||
@@ -49,39 +62,47 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{(() => {
|
||||||
<>
|
if (isLoading)
|
||||||
<SkeletonText className="block h-16 w-full" />
|
return (
|
||||||
<SkeletonText className="block h-16 w-full" />
|
<>
|
||||||
</>
|
<SkeletonText className="block h-16 w-full" />
|
||||||
) : data && data.busy.length > 0 ? (
|
<SkeletonText className="block h-16 w-full" />
|
||||||
data.busy
|
</>
|
||||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
);
|
||||||
.map((slot: IBusySlot) => (
|
|
||||||
<div
|
if (data && (data.busy.length > 0 || overrides.length > 0))
|
||||||
key={dayjs(slot.start).format("HH:mm")}
|
return [...data.busy, ...overrides]
|
||||||
className="overflow-hidden rounded-md bg-neutral-100">
|
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||||
<div className="px-4 py-5 text-black sm:p-6">
|
.map((slot: IBusySlot) => (
|
||||||
{t("calendar_shows_busy_between")}{" "}
|
<div
|
||||||
<span className="font-medium text-neutral-800" title={dayjs(slot.start).format("HH:mm")}>
|
key={dayjs(slot.start).format("HH:mm")}
|
||||||
{dayjs(slot.start).format("HH:mm")}
|
className="overflow-hidden rounded-md bg-neutral-100"
|
||||||
</span>{" "}
|
data-testid="troubleshooter-busy-time">
|
||||||
{t("and")}{" "}
|
<div className="px-4 py-5 text-black sm:p-6">
|
||||||
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
|
{t("calendar_shows_busy_between")}{" "}
|
||||||
{dayjs(slot.end).format("HH:mm")}
|
<span
|
||||||
</span>{" "}
|
className="font-medium text-neutral-800"
|
||||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
title={dayjs(slot.start).format("HH:mm")}>
|
||||||
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
{dayjs(slot.start).format("HH:mm")}
|
||||||
{slot.title && ` - (${slot.title})`}
|
</span>{" "}
|
||||||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
{t("and")}{" "}
|
||||||
|
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
|
||||||
|
{dayjs(slot.end).format("HH:mm")}
|
||||||
|
</span>{" "}
|
||||||
|
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
||||||
|
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
||||||
|
{slot.title && ` - (${slot.title})`}
|
||||||
|
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
));
|
||||||
))
|
return (
|
||||||
) : (
|
<div className="overflow-hidden rounded-md bg-neutral-100">
|
||||||
<div className="overflow-hidden rounded-md bg-neutral-100">
|
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
|
|||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma, { baseUserSelect } from "@calcom/prisma";
|
import prisma, { baseUserSelect } from "@calcom/prisma";
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Button, EmailInput, Icon } from "@calcom/ui";
|
import { Button, EmailInput, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
|
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -56,26 +57,41 @@ function redirectToExternalUrl(url: string) {
|
|||||||
function RedirectionToast({ url }: { url: string }) {
|
function RedirectionToast({ url }: { url: string }) {
|
||||||
const [timeRemaining, setTimeRemaining] = useState(10);
|
const [timeRemaining, setTimeRemaining] = useState(10);
|
||||||
const [isToastVisible, setIsToastVisible] = useState(true);
|
const [isToastVisible, setIsToastVisible] = useState(true);
|
||||||
const parsedSuccessUrl = new URL(document.URL);
|
|
||||||
const parsedExternalUrl = new URL(url);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
|
||||||
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
|
||||||
parsedSuccessUrl.searchParams.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlWithSuccessParams =
|
|
||||||
parsedExternalUrl.origin +
|
|
||||||
parsedExternalUrl.pathname +
|
|
||||||
"?" +
|
|
||||||
parsedSuccessUrl.searchParams.toString() +
|
|
||||||
parsedExternalUrl.hash;
|
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const timerRef = useRef<number | null>(null);
|
const timerRef = useRef<number | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const { cancel: isCancellationMode } = querySchema.parse(router.query);
|
||||||
|
const urlWithSuccessParamsRef = useRef<string | null>();
|
||||||
|
if (isCancellationMode && timerRef.current) {
|
||||||
|
setIsToastVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isToastVisible && timerRef.current) {
|
||||||
|
window.clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
}, [isToastVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsedExternalUrl = new URL(url);
|
||||||
|
|
||||||
|
const parsedSuccessUrl = new URL(document.URL);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
||||||
|
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
||||||
|
parsedSuccessUrl.searchParams.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlWithSuccessParams =
|
||||||
|
parsedExternalUrl.origin +
|
||||||
|
parsedExternalUrl.pathname +
|
||||||
|
"?" +
|
||||||
|
parsedSuccessUrl.searchParams.toString() +
|
||||||
|
parsedExternalUrl.hash;
|
||||||
|
urlWithSuccessParamsRef.current = urlWithSuccessParams;
|
||||||
|
|
||||||
timerRef.current = window.setInterval(() => {
|
timerRef.current = window.setInterval(() => {
|
||||||
if (timeRemaining > 0) {
|
if (timeRemaining > 0) {
|
||||||
setTimeRemaining((timeRemaining) => {
|
setTimeRemaining((timeRemaining) => {
|
||||||
@@ -89,7 +105,7 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timerRef.current as number);
|
window.clearInterval(timerRef.current as number);
|
||||||
};
|
};
|
||||||
}, [timeRemaining, urlWithSuccessParams]);
|
}, [timeRemaining, url]);
|
||||||
|
|
||||||
if (!isToastVisible) {
|
if (!isToastVisible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -112,7 +128,9 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redirectToExternalUrl(urlWithSuccessParams);
|
if (urlWithSuccessParamsRef.current) {
|
||||||
|
redirectToExternalUrl(urlWithSuccessParamsRef.current);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
@@ -123,7 +141,6 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsToastVisible(false);
|
setIsToastVisible(false);
|
||||||
window.clearInterval(timerRef.current as number);
|
|
||||||
}}
|
}}
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<Icon.FiX className="h-6 w-6 text-white" />
|
<Icon.FiX className="h-6 w-6 text-white" />
|
||||||
@@ -151,6 +168,7 @@ const querySchema = z.object({
|
|||||||
changes: stringToBoolean,
|
changes: stringToBoolean,
|
||||||
reschedule: stringToBoolean,
|
reschedule: stringToBoolean,
|
||||||
isSuccessBookingPage: z.string().optional(),
|
isSuccessBookingPage: z.string().optional(),
|
||||||
|
formerTime: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Success(props: SuccessProps) {
|
export default function Success(props: SuccessProps) {
|
||||||
@@ -162,26 +180,33 @@ export default function Success(props: SuccessProps) {
|
|||||||
isSuccessBookingPage,
|
isSuccessBookingPage,
|
||||||
cancel: isCancellationMode,
|
cancel: isCancellationMode,
|
||||||
changes,
|
changes,
|
||||||
|
formerTime,
|
||||||
} = querySchema.parse(router.query);
|
} = querySchema.parse(router.query);
|
||||||
|
|
||||||
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
|
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
|
||||||
? props.bookingInfo.location[0] || ""
|
? props.bookingInfo.location[0]
|
||||||
: props.bookingInfo.location || "";
|
: // If there is no location set then we default to Cal Video
|
||||||
|
"integrations:daily";
|
||||||
|
|
||||||
if (!location) {
|
if (!location) {
|
||||||
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
||||||
console.error(`No location found `);
|
console.error(`No location found `);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = props.bookingInfo?.user?.name;
|
|
||||||
const email = props.bookingInfo?.user?.email;
|
const email = props.bookingInfo?.user?.email;
|
||||||
const status = props.bookingInfo?.status;
|
const status = props.bookingInfo?.status;
|
||||||
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
||||||
const cancellationReason = props.bookingInfo.cancellationReason;
|
const cancellationReason = props.bookingInfo.cancellationReason;
|
||||||
|
|
||||||
|
const attendeeName =
|
||||||
|
typeof props?.bookingInfo?.attendees?.[0]?.name === "string"
|
||||||
|
? props?.bookingInfo?.attendees?.[0]?.name
|
||||||
|
: "Nameless";
|
||||||
|
|
||||||
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
@@ -203,8 +228,6 @@ export default function Success(props: SuccessProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const attendeeName = typeof name === "string" ? name : "Nameless";
|
|
||||||
|
|
||||||
const eventNameObject = {
|
const eventNameObject = {
|
||||||
attendeeName,
|
attendeeName,
|
||||||
eventType: props.eventType.title,
|
eventType: props.eventType.title,
|
||||||
@@ -317,6 +340,19 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
||||||
|
{!isEmbed && (
|
||||||
|
<EventReservationSchema
|
||||||
|
reservationId={bookingInfo.uid}
|
||||||
|
eventName={eventName}
|
||||||
|
startTime={bookingInfo.startTime}
|
||||||
|
endTime={bookingInfo.endTime}
|
||||||
|
organizer={bookingInfo.user}
|
||||||
|
attendees={bookingInfo.attendees}
|
||||||
|
location={locationToDisplay}
|
||||||
|
description={bookingInfo.description}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{userIsOwner && !isEmbed && (
|
{userIsOwner && !isEmbed && (
|
||||||
<div className="mt-2 ml-4 -mb-4">
|
<div className="mt-2 ml-4 -mb-4">
|
||||||
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
||||||
@@ -331,7 +367,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
||||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||||
{eventType.successRedirectUrl ? <RedirectionToast url={eventType.successRedirectUrl} /> : null}{" "}
|
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
|
||||||
|
<RedirectionToast url={eventType.successRedirectUrl} />
|
||||||
|
) : null}{" "}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "text-center" : "",
|
shouldAlignCentrally ? "text-center" : "",
|
||||||
@@ -389,9 +427,11 @@ export default function Success(props: SuccessProps) {
|
|||||||
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p>
|
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
||||||
{isCancelled && cancellationReason && (
|
{(isCancelled || reschedule) && cancellationReason && (
|
||||||
<>
|
<>
|
||||||
<div className="font-medium">{t("reason")}</div>
|
<div className="font-medium">
|
||||||
|
{isCancelled ? t("reason") : t("reschedule_reason_success_page")}
|
||||||
|
</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
|
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -399,6 +439,19 @@ export default function Success(props: SuccessProps) {
|
|||||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
<div className="col-span-2 mb-6 last:mb-0">{eventName}</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 && (
|
||||||
|
<p className="line-through">
|
||||||
|
<RecurringBookings
|
||||||
|
eventType={props.eventType}
|
||||||
|
duration={calculatedDuration}
|
||||||
|
recurringBookings={props.recurringBookings}
|
||||||
|
allRemainingBookings={allRemainingBookings}
|
||||||
|
date={dayjs(formerTime)}
|
||||||
|
is24h={is24h}
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<RecurringBookings
|
<RecurringBookings
|
||||||
eventType={props.eventType}
|
eventType={props.eventType}
|
||||||
duration={calculatedDuration}
|
duration={calculatedDuration}
|
||||||
@@ -454,10 +507,37 @@ export default function Success(props: SuccessProps) {
|
|||||||
)}
|
)}
|
||||||
{customInputs &&
|
{customInputs &&
|
||||||
Object.keys(customInputs).map((key) => {
|
Object.keys(customInputs).map((key) => {
|
||||||
|
// This breaks if you have two label that are the same.
|
||||||
|
// TODO: Fix this in another PR
|
||||||
const customInput = customInputs[key as keyof typeof customInputs];
|
const customInput = customInputs[key as keyof typeof customInputs];
|
||||||
|
const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{customInput !== "" && (
|
{eventTypeCustomFound?.type === "RADIO" && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
||||||
|
{eventTypeCustomFound.label}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 mt-1 mb-2">
|
||||||
|
{eventTypeCustomFound.options &&
|
||||||
|
eventTypeCustomFound.options.map((option) => {
|
||||||
|
const selected = option.label == customInput;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.label}
|
||||||
|
className={classNames(
|
||||||
|
"flex space-x-1",
|
||||||
|
!selected && "text-gray-500"
|
||||||
|
)}>
|
||||||
|
<p>{option.label}</p>
|
||||||
|
<span>{option.label === customInput && "✅"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
|
||||||
<>
|
<>
|
||||||
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
|
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
|
||||||
<div className="col-span-3 mt-2 mb-2">
|
<div className="col-span-3 mt-2 mb-2">
|
||||||
@@ -727,10 +807,10 @@ export function RecurringBookings({
|
|||||||
{eventType.recurringEvent?.count &&
|
{eventType.recurringEvent?.count &&
|
||||||
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
|
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
|
||||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -745,10 +825,10 @@ export function RecurringBookings({
|
|||||||
{eventType.recurringEvent?.count &&
|
{eventType.recurringEvent?.count &&
|
||||||
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
|
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
|
||||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -761,10 +841,10 @@ export function RecurringBookings({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(isCancelled ? "line-through" : "")}>
|
<div className={classNames(isCancelled ? "line-through" : "")}>
|
||||||
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(date, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(date, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||||
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -785,6 +865,7 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
|
customInputs: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
@@ -794,7 +875,6 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
name: true,
|
name: true,
|
||||||
username: true,
|
username: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
plan: true,
|
|
||||||
theme: true,
|
theme: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
@@ -851,22 +931,25 @@ const handleSeatsEventTypeOnBooking = (
|
|||||||
seatsShowAttendees: boolean | null;
|
seatsShowAttendees: boolean | null;
|
||||||
[x: string | number | symbol]: unknown;
|
[x: string | number | symbol]: unknown;
|
||||||
},
|
},
|
||||||
booking: Partial<
|
bookingInfo: Partial<
|
||||||
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
|
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
|
||||||
>,
|
>,
|
||||||
email: string
|
email: string
|
||||||
) => {
|
) => {
|
||||||
if (eventType?.seatsPerTimeSlot !== null) {
|
if (eventType?.seatsPerTimeSlot !== null) {
|
||||||
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
||||||
delete booking.description;
|
delete bookingInfo.description;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!eventType.seatsShowAttendees) {
|
if (!eventType.seatsShowAttendees) {
|
||||||
const attendee = booking?.attendees?.find((a) => a.email === email);
|
const attendee = bookingInfo?.attendees?.find((a) => {
|
||||||
booking["attendees"] = attendee ? [attendee] : [];
|
return a.email === email;
|
||||||
|
});
|
||||||
|
|
||||||
|
bookingInfo["attendees"] = attendee ? [attendee] : [];
|
||||||
}
|
}
|
||||||
return;
|
return bookingInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
@@ -945,7 +1028,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
select: baseUserSelect,
|
select: baseUserSelect,
|
||||||
});
|
});
|
||||||
if (user) {
|
if (user) {
|
||||||
eventTypeRaw.users.push(user);
|
eventTypeRaw.users.push({
|
||||||
|
...user,
|
||||||
|
avatar: "",
|
||||||
|
allowDynamicBooking: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +1048,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
};
|
};
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
@@ -972,7 +1060,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bookingInfo !== null && email) {
|
if (bookingInfo !== null && email && eventType.seatsPerTimeSlot) {
|
||||||
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
|
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,29 @@ import { Fragment } from "react";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
|
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
|
||||||
|
import { filterQuerySchema, useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Alert, BookingLayout, Button, EmptyScreen, Icon } from "@calcom/ui";
|
import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||||
|
|
||||||
import BookingListItem from "@components/booking/BookingListItem";
|
import BookingListItem from "@components/booking/BookingListItem";
|
||||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||||
|
|
||||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
|
type BookingListingStatus = z.infer<typeof filterQuerySchema>["status"];
|
||||||
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
||||||
|
|
||||||
|
type RecurringInfo = {
|
||||||
|
recurringEventId: string | null;
|
||||||
|
count: number;
|
||||||
|
firstDate: Date | null;
|
||||||
|
bookings: { [key: string]: Date[] };
|
||||||
|
};
|
||||||
|
|
||||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||||
|
|
||||||
const descriptionByStatus: Record<BookingListingStatus, string> = {
|
const descriptionByStatus: Record<BookingListingStatus, string> = {
|
||||||
@@ -32,12 +43,19 @@ const querySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Bookings() {
|
export default function Bookings() {
|
||||||
|
const { data: filterQuery } = useFilterQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const };
|
const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const };
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
||||||
{ status, limit: 10 },
|
{
|
||||||
|
limit: 10,
|
||||||
|
filters: {
|
||||||
|
...filterQuery,
|
||||||
|
status: filterQuery.status ?? status,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// first render has status `undefined`
|
// first render has status `undefined`
|
||||||
enabled: router.isReady,
|
enabled: router.isReady,
|
||||||
@@ -73,6 +91,18 @@ export default function Bookings() {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let recurringInfoToday: RecurringInfo | undefined;
|
||||||
|
|
||||||
|
const bookingsToday =
|
||||||
|
query.data?.pages.map((page) =>
|
||||||
|
page.bookings.filter((booking: BookingOutput) => {
|
||||||
|
recurringInfoToday = page.recurringInfo.find(
|
||||||
|
(info) => info.recurringEventId === booking.recurringEventId
|
||||||
|
);
|
||||||
|
return new Date(booking.startTime).toDateString() === new Date().toDateString();
|
||||||
|
})
|
||||||
|
)[0] || [];
|
||||||
|
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -83,48 +113,68 @@ export default function Bookings() {
|
|||||||
)}
|
)}
|
||||||
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
||||||
{query.status === "success" && !isEmpty && (
|
{query.status === "success" && !isEmpty && (
|
||||||
<div className="pt-2 xl:pt-0">
|
<>
|
||||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
{!!bookingsToday.length && status === "upcoming" && (
|
||||||
{/* TODO: add today only for the current day
|
<div className="mb-6 pt-2 xl:pt-0">
|
||||||
<p className="pb-3 text-xs font-medium leading-4 text-gray-500 uppercase">{t("today")}</p>
|
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||||
*/}
|
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
|
||||||
|
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||||
<div className="overflow-hidden rounded-md border border-gray-200">
|
<table className="w-full max-w-full table-fixed">
|
||||||
<table className="w-full max-w-full table-fixed">
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
<Fragment>
|
||||||
{query.data.pages.map((page, index) => (
|
{bookingsToday.map((booking: BookingOutput) => (
|
||||||
<Fragment key={index}>
|
|
||||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
|
||||||
const recurringInfo = page.recurringInfo.find(
|
|
||||||
(info) => info.recurringEventId === booking.recurringEventId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<BookingListItem
|
<BookingListItem
|
||||||
key={booking.id}
|
key={booking.id}
|
||||||
listingStatus={status}
|
listingStatus={status}
|
||||||
recurringInfo={recurringInfo}
|
recurringInfo={recurringInfoToday}
|
||||||
{...booking}
|
{...booking}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
</Fragment>
|
||||||
</Fragment>
|
</tbody>
|
||||||
))}
|
</table>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="pt-2 xl:pt-0">
|
||||||
|
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p>
|
||||||
|
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||||
|
<table className="w-full max-w-full table-fixed">
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
||||||
|
{query.data.pages.map((page, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
||||||
|
const recurringInfo = page.recurringInfo.find(
|
||||||
|
(info) => info.recurringEventId === booking.recurringEventId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<BookingListItem
|
||||||
|
key={booking.id}
|
||||||
|
listingStatus={status}
|
||||||
|
recurringInfo={recurringInfo}
|
||||||
|
{...booking}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center" ref={buttonInView.ref}>
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
loading={query.isFetchingNextPage}
|
||||||
|
disabled={!query.hasNextPage}
|
||||||
|
onClick={() => query.fetchNextPage()}>
|
||||||
|
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center" ref={buttonInView.ref}>
|
</>
|
||||||
<Button
|
|
||||||
color="minimal"
|
|
||||||
loading={query.isFetchingNextPage}
|
|
||||||
disabled={!query.hasNextPage}
|
|
||||||
onClick={() => query.fetchNextPage()}>
|
|
||||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{query.status === "success" && isEmpty && (
|
{query.status === "success" && isEmpty && (
|
||||||
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
|
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Icon.FiCalendar}
|
Icon={Icon.FiCalendar}
|
||||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||||
@@ -140,14 +190,16 @@ export default function Bookings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (ctx) => {
|
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
|
const ssg = await ssgInit(ctx);
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
if (!params.success) return { notFound: true };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
status: params.data.status,
|
status: params.data.status,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
name: u.name,
|
name: u.name,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
hideBranding: u.hideBranding,
|
hideBranding: u.hideBranding,
|
||||||
plan: u.plan,
|
|
||||||
timeZone: u.timeZone,
|
timeZone: u.timeZone,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@@ -158,7 +156,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
away: user.away,
|
away: user.away,
|
||||||
isDynamicGroup: false,
|
isDynamicGroup: false,
|
||||||
profile,
|
profile,
|
||||||
plan: user.plan,
|
|
||||||
date,
|
date,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
@@ -166,6 +163,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
previousPage: context.req.headers.referer ?? null,
|
previousPage: context.req.headers.referer ?? null,
|
||||||
booking,
|
booking,
|
||||||
users: [user.username],
|
users: [user.username],
|
||||||
|
isBrandingHidden: user.hideBranding,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,7 @@ import { JSONObject } from "superjson/dist/types";
|
|||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
@@ -79,6 +79,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
...e,
|
...e,
|
||||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||||
|
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
||||||
schedulingType: null,
|
schedulingType: null,
|
||||||
users: users.map((u) => ({
|
users: users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -8,23 +9,24 @@ import { useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
||||||
import getApps, { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
||||||
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
||||||
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Form, showToast } from "@calcom/ui";
|
import { Form, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||||
@@ -35,10 +37,12 @@ import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
|||||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||||
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
||||||
import { EventTeamTab } from "@components/eventtype/EventTeamTab";
|
import { EventTeamTab } from "@components/eventtype/EventTeamTab";
|
||||||
|
import { EventTeamWebhooksTab } from "@components/eventtype/EventTeamWebhooksTab";
|
||||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||||
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
||||||
|
|
||||||
import { getTranslation } from "@server/lib/i18n";
|
import { getTranslation } from "@server/lib/i18n";
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type FormValues = {
|
export type FormValues = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -63,7 +67,7 @@ export type FormValues = {
|
|||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
}[];
|
}[];
|
||||||
customInputs: EventTypeCustomInput[];
|
customInputs: CustomInputParsed[];
|
||||||
users: string[];
|
users: string[];
|
||||||
schedule: number;
|
schedule: number;
|
||||||
periodType: PeriodType;
|
periodType: PeriodType;
|
||||||
@@ -87,9 +91,21 @@ export type FormValues = {
|
|||||||
bookingLimits?: BookingLimit;
|
bookingLimits?: BookingLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
tabName: z
|
tabName: z
|
||||||
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
|
.enum([
|
||||||
|
"setup",
|
||||||
|
"availability",
|
||||||
|
"apps",
|
||||||
|
"limits",
|
||||||
|
"recurring",
|
||||||
|
"team",
|
||||||
|
"advanced",
|
||||||
|
"workflows",
|
||||||
|
"webhooks",
|
||||||
|
])
|
||||||
.optional()
|
.optional()
|
||||||
.default("setup"),
|
.default("setup"),
|
||||||
});
|
});
|
||||||
@@ -189,6 +205,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
),
|
),
|
||||||
metadata,
|
metadata,
|
||||||
},
|
},
|
||||||
|
resolver: zodResolver(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
// Length if string, is converted to a number or it can be a number
|
||||||
|
// Make it optional because it's not submitted from all tabs of the page
|
||||||
|
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||||
|
})
|
||||||
|
// TODO: Add schema for other fields later.
|
||||||
|
.passthrough()
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||||
@@ -233,6 +259,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
workflows={eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow)}
|
workflows={eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -244,7 +271,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
team={team}
|
team={team}
|
||||||
isUpdateMutationLoading={updateMutation.isLoading}
|
isUpdateMutationLoading={updateMutation.isLoading}
|
||||||
formMethods={formMethods}
|
formMethods={formMethods}
|
||||||
disableBorder={tabName === "apps" || tabName === "workflows"}
|
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||||
currentUserMembership={props.currentUserMembership}>
|
currentUserMembership={props.currentUserMembership}>
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
@@ -261,6 +288,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
recurringEvent,
|
recurringEvent,
|
||||||
locations,
|
locations,
|
||||||
metadata,
|
metadata,
|
||||||
|
customInputs,
|
||||||
// We don't need to send send these values to the backend
|
// We don't need to send send these values to the backend
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
seatsPerTimeSlotEnabled,
|
seatsPerTimeSlotEnabled,
|
||||||
@@ -278,7 +306,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
if (metadata?.multipleDuration.length < 1) {
|
if (metadata?.multipleDuration.length < 1) {
|
||||||
throw new Error(t("event_setup_multiple_duration_error"));
|
throw new Error(t("event_setup_multiple_duration_error"));
|
||||||
} else {
|
} else {
|
||||||
if (!metadata?.multipleDuration?.includes(input.length)) {
|
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
||||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,6 +326,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
seatsShowAttendees,
|
seatsShowAttendees,
|
||||||
metadata,
|
metadata,
|
||||||
|
customInputs,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div ref={animationParentRef} className="space-y-6">
|
<div ref={animationParentRef} className="space-y-6">
|
||||||
@@ -312,6 +341,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const { req, query } = context;
|
const { req, query } = context;
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
if (Number.isNaN(typeParam)) {
|
if (Number.isNaN(typeParam)) {
|
||||||
return {
|
return {
|
||||||
@@ -334,7 +364,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
id: true,
|
id: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
email: true,
|
email: true,
|
||||||
plan: true,
|
|
||||||
locale: true,
|
locale: true,
|
||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
});
|
});
|
||||||
@@ -430,6 +459,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
destinationCalendar: true,
|
destinationCalendar: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
seatsShowAttendees: true,
|
seatsShowAttendees: true,
|
||||||
|
webhooks: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
subscriberUrl: true,
|
||||||
|
payloadTemplate: true,
|
||||||
|
active: true,
|
||||||
|
eventTriggers: true,
|
||||||
|
secret: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
include: {
|
include: {
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -461,6 +501,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const credentials = await prisma.credential.findMany({
|
const credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
|
app: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -468,6 +511,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
appId: true,
|
appId: true,
|
||||||
|
invalid: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -495,6 +539,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
|
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
|
||||||
const parsedMetaData = newMetadata;
|
const parsedMetaData = newMetadata;
|
||||||
|
|
||||||
|
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...restEventType,
|
...restEventType,
|
||||||
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
||||||
@@ -502,6 +548,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
||||||
locations: locations as unknown as LocationObject[],
|
locations: locations as unknown as LocationObject[],
|
||||||
metadata: parsedMetaData,
|
metadata: parsedMetaData,
|
||||||
|
customInputs: parsedCustomInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// backwards compat
|
// backwards compat
|
||||||
@@ -517,7 +564,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
}
|
}
|
||||||
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
||||||
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
||||||
const integrations = getApps(credentials);
|
const integrations = await getEnabledApps(credentials);
|
||||||
const locationOptions = getLocationOptions(integrations, t);
|
const locationOptions = getLocationOptions(integrations, t);
|
||||||
|
|
||||||
const eventTypeObject = Object.assign({}, eventType, {
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
@@ -546,6 +593,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
team: eventTypeObject.team || null,
|
team: eventTypeObject.team || null,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
currentUserMembership,
|
currentUserMembership,
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import Head from "next/head";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
import React, { Fragment, useEffect, useState } from "react";
|
||||||
|
|
||||||
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
|
import {
|
||||||
|
CreateEventTypeButton,
|
||||||
|
EventTypeDescriptionLazy as EventTypeDescription,
|
||||||
|
} from "@calcom/features/eventtypes/components";
|
||||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
@@ -22,7 +25,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
EventTypeDescription,
|
|
||||||
Icon,
|
Icon,
|
||||||
Shell,
|
Shell,
|
||||||
showToast,
|
showToast,
|
||||||
@@ -38,6 +40,8 @@ import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
|||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||||
|
|
||||||
@@ -175,22 +179,17 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
}
|
}
|
||||||
|
|
||||||
// inject selection data into url for correct router history
|
// inject selection data into url for correct router history
|
||||||
const openModal = (group: EventTypeGroup, type: EventType) => {
|
const openDuplicateModal = (eventType: EventType) => {
|
||||||
const query = {
|
const query = {
|
||||||
...router.query,
|
...router.query,
|
||||||
dialog: "new-eventtype",
|
dialog: "duplicate-event-type",
|
||||||
eventPage: group.profile.slug,
|
title: eventType.title,
|
||||||
title: type.title,
|
description: eventType.description,
|
||||||
slug: type.slug,
|
slug: eventType.slug,
|
||||||
description: type.description,
|
id: eventType.id,
|
||||||
length: type.length,
|
length: eventType.length,
|
||||||
type: type.schedulingType,
|
|
||||||
teamId: group.teamId,
|
|
||||||
locations: encodeURIComponent(JSON.stringify(type.locations)),
|
|
||||||
};
|
};
|
||||||
if (!group.teamId) {
|
|
||||||
delete query.teamId;
|
|
||||||
}
|
|
||||||
router.push(
|
router.push(
|
||||||
{
|
{
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
@@ -259,7 +258,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
const calLink = `${CAL_URL}/${embedLink}`;
|
const calLink = `${CAL_URL}/${embedLink}`;
|
||||||
return (
|
return (
|
||||||
<li key={type.id}>
|
<li key={type.id}>
|
||||||
<div className="flex w-full items-center justify-between hover:bg-neutral-50">
|
<div className="flex w-full items-center justify-between hover:bg-gray-50">
|
||||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
||||||
{!(firstItem && firstItem.id === type.id) && (
|
{!(firstItem && firstItem.id === type.id) && (
|
||||||
<button
|
<button
|
||||||
@@ -359,7 +358,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
type="button"
|
type="button"
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
StartIcon={Icon.FiCopy}
|
StartIcon={Icon.FiCopy}
|
||||||
onClick={() => openModal(group, type)}>
|
onClick={() => openDuplicateModal(type)}>
|
||||||
{t("duplicate") as string}
|
{t("duplicate") as string}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -467,7 +466,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
className="w-full rounded-none"
|
className="w-full rounded-none"
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
StartIcon={Icon.FiCopy}
|
StartIcon={Icon.FiCopy}
|
||||||
onClick={() => openModal(group, type)}>
|
onClick={() => openDuplicateModal(type)}>
|
||||||
{t("duplicate") as string}
|
{t("duplicate") as string}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -531,9 +530,9 @@ const EventTypeListHeading = ({
|
|||||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
||||||
<a className="font-bold">{profile?.name || ""}</a>
|
<a className="font-bold">{profile?.name || ""}</a>
|
||||||
</Link>
|
</Link>
|
||||||
{membershipCount && (
|
{membershipCount && teamId && (
|
||||||
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
|
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
|
||||||
<Link href="/settings/teams">
|
<Link href={`/settings/teams/${teamId}/members`}>
|
||||||
<a>
|
<a>
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
||||||
@@ -579,12 +578,9 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
|
|||||||
|
|
||||||
const EventTypesPage = () => {
|
const EventTypesPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
|
||||||
<title>Home | {APP_NAME}</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<Shell
|
<Shell
|
||||||
heading={t("event_types_page_title") as string}
|
heading={t("event_types_page_title") as string}
|
||||||
subtitle={t("event_types_page_subtitle") as string}
|
subtitle={t("event_types_page_subtitle") as string}
|
||||||
@@ -622,4 +618,14 @@ const EventTypesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default EventTypesPage;
|
export default EventTypesPage;
|
||||||
@@ -11,15 +11,14 @@ import { User } from "@calcom/prisma/client";
|
|||||||
import { Button, StepCard, Steps } from "@calcom/ui";
|
import { Button, StepCard, Steps } from "@calcom/ui";
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||||
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
||||||
|
|
||||||
interface IOnboardingPageProps {
|
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INITIAL_STEP = "user-settings";
|
const INITIAL_STEP = "user-settings";
|
||||||
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
||||||
@@ -165,7 +164,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
weekStart: true,
|
weekStart: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!booking) {
|
if (!booking) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
|
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +66,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
||||||
"/" +
|
"/" +
|
||||||
eventType?.slug;
|
eventType?.slug;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
|
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
||||||
|
|
||||||
|
export { default } from "../[uid]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
|
||||||
|
|
||||||
function AdminAppsView() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Meta title="Apps" description="apps_description" />
|
|
||||||
<h1>App listing</h1>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
|
||||||
|
|
||||||
export default AdminAppsView;
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
function AdminAppsView() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("apps")} description={t("admin_apps_description")} />
|
||||||
|
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminAppsView;
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
|
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminView() {
|
function AdminView() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Admin" description="Impersonation" />
|
<Meta title={t("admin")} description={t("impersonation")} />
|
||||||
<form
|
<form
|
||||||
className="mb-6 w-full sm:w-1/2"
|
className="mb-6 w-full sm:w-1/2"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -21,7 +24,7 @@ function AdminView() {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<TextField
|
<TextField
|
||||||
containerClassName="w-full"
|
containerClassName="w-full"
|
||||||
name="Impersonate User"
|
name={t("user_impersonation_heading")}
|
||||||
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
|
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
hint={t("impersonate_user_tip")}
|
hint={t("impersonate_user_tip")}
|
||||||
@@ -36,4 +39,14 @@ function AdminView() {
|
|||||||
|
|
||||||
AdminView.getLayout = getLayout;
|
AdminView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminView;
|
export default AdminView;
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminAppsView() {
|
function AdminAppsView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -11,4 +15,14 @@ function AdminAppsView() {
|
|||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminAppsView;
|
export default AdminAppsView;
|
||||||
@@ -1,14 +1,30 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminUsersView() {
|
function AdminUsersView() {
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Users" description="users_description" />
|
<Meta title={t("users")} description={t("users_description")} />
|
||||||
<h1>Users listing</h1>
|
<h1>{t("users_listing")}</h1>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminUsersView.getLayout = getLayout;
|
AdminUsersView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminUsersView;
|
export default AdminUsersView;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
import { HelpScout, useChat } from "react-live-chat-loader";
|
||||||
@@ -8,6 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
|
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
interface CtaRowProps {
|
interface CtaRowProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -33,7 +36,6 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
|||||||
const BillingView = () => {
|
const BillingView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const isPro = user?.plan === "PRO";
|
|
||||||
const [, loadChat] = useChat();
|
const [, loadChat] = useChat();
|
||||||
const [showChat, setShowChat] = useState(false);
|
const [showChat, setShowChat] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -50,21 +52,16 @@ const BillingView = () => {
|
|||||||
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
||||||
<div className="space-y-6 text-sm sm:space-y-8">
|
<div className="space-y-6 text-sm sm:space-y-8">
|
||||||
<CtaRow
|
<CtaRow
|
||||||
className={classNames(!isPro && "pointer-events-none opacity-30")}
|
|
||||||
title={t("billing_manage_details_title")}
|
title={t("billing_manage_details_title")}
|
||||||
description={t("billing_manage_details_description")}>
|
description={t("billing_manage_details_description")}>
|
||||||
<Button
|
<Button color="primary" href={billingHref} target="_blank" EndIcon={Icon.FiExternalLink}>
|
||||||
color={isPro ? "primary" : "secondary"}
|
|
||||||
href={billingHref}
|
|
||||||
target="_blank"
|
|
||||||
EndIcon={Icon.FiExternalLink}>
|
|
||||||
{t("billing_portal")}
|
{t("billing_portal")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
|
|
||||||
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
||||||
<Button color="secondary" onClick={onContactSupportClick}>
|
<Button color="secondary" onClick={onContactSupportClick}>
|
||||||
{t("billing_help_cta")}
|
{t("contact_support")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
||||||
@@ -75,4 +72,14 @@ const BillingView = () => {
|
|||||||
|
|
||||||
BillingView.getLayout = getLayout;
|
BillingView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default BillingView;
|
export default BillingView;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
SkeletonLoader,
|
SkeletonLoader,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const ApiKeysView = () => {
|
const ApiKeysView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -93,4 +96,14 @@ const ApiKeysView = () => {
|
|||||||
|
|
||||||
ApiKeysView.getLayout = getLayout;
|
ApiKeysView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ApiKeysView;
|
export default ApiKeysView;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
@@ -17,9 +19,12 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||||
@@ -41,7 +46,7 @@ const SkeletonLoader = () => {
|
|||||||
|
|
||||||
const AppearanceView = () => {
|
const AppearanceView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const session = useSession();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
|
|
||||||
@@ -68,7 +73,7 @@ const AppearanceView = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
@@ -85,7 +90,7 @@ const AppearanceView = () => {
|
|||||||
theme: values.theme || null,
|
theme: values.theme || null,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<Meta title="Appearance" description="Manage settings for your booking appearance" />
|
<Meta title={t("appearance")} description={t("appearance_description")} />
|
||||||
<div className="mb-6 flex items-center text-sm">
|
<div className="mb-6 flex items-center text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">{t("theme")}</p>
|
<p className="font-semibold">{t("theme")}</p>
|
||||||
@@ -180,10 +185,11 @@ const AppearanceView = () => {
|
|||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
<Switch
|
<Switch
|
||||||
id="hideBranding"
|
id="hideBranding"
|
||||||
|
disabled={!session.data?.user.belongsToActiveTeam}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
|
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
|
||||||
}
|
}
|
||||||
checked={value}
|
checked={!session.data?.user.belongsToActiveTeam ? false : value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,6 +210,16 @@ const AppearanceView = () => {
|
|||||||
|
|
||||||
AppearanceView.getLayout = getLayout;
|
AppearanceView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AppearanceView;
|
export default AppearanceView;
|
||||||
interface ThemeLabelProps {
|
interface ThemeLabelProps {
|
||||||
variant: "light" | "dark" | "system";
|
variant: "light" | "dark" | "system";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -22,12 +23,15 @@ import {
|
|||||||
SkeletonButton,
|
SkeletonButton,
|
||||||
SkeletonContainer,
|
SkeletonContainer,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -66,15 +70,17 @@ const CalendarsView = () => {
|
|||||||
async onSettled() {
|
async onSettled() {
|
||||||
await utils.viewer.connectedCalendars.invalidate();
|
await utils.viewer.connectedCalendars.invalidate();
|
||||||
},
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("calendar_updated_successfully"), "success");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast(t("unexpected_error_try_again"), "error");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta
|
<Meta title={t("calendars")} description={t("calendars_description")} CTA={<AddCalendarButton />} />
|
||||||
title="Calendars"
|
|
||||||
description="Configure how your event types interact with your calendars"
|
|
||||||
CTA={<AddCalendarButton />}
|
|
||||||
/>
|
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader />}
|
||||||
@@ -110,7 +116,7 @@ const CalendarsView = () => {
|
|||||||
{t("check_for_conflicts")}
|
{t("check_for_conflicts")}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
|
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
|
||||||
<List>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
<Fragment key={item.credentialId}>
|
<Fragment key={item.credentialId}>
|
||||||
{item.error && item.error.message && (
|
{item.error && item.error.message && (
|
||||||
@@ -137,7 +143,7 @@ const CalendarsView = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item?.error === undefined && item.calendars && (
|
{item?.error === undefined && item.calendars && (
|
||||||
<ListItem className="flex-col">
|
<ListItem className="flex-col rounded-md">
|
||||||
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
|
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
@@ -164,7 +170,7 @@ const CalendarsView = () => {
|
|||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
trashIcon
|
trashIcon
|
||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
buttonProps={{ size: "icon", color: "secondary" }}
|
buttonProps={{ className: "border border-gray-300" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +178,7 @@ const CalendarsView = () => {
|
|||||||
<p className="px-2 pt-4 text-sm text-neutral-500">
|
<p className="px-2 pt-4 text-sm text-neutral-500">
|
||||||
{t("toggle_calendars_conflict")}
|
{t("toggle_calendars_conflict")}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 px-2 pt-4">
|
<ul className="space-y-2 p-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
key={cal.externalId}
|
key={cal.externalId}
|
||||||
@@ -224,4 +230,14 @@ const CalendarsView = () => {
|
|||||||
|
|
||||||
CalendarsView.getLayout = getLayout;
|
CalendarsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default CalendarsView;
|
export default CalendarsView;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -24,9 +25,12 @@ import {
|
|||||||
SkeletonText,
|
SkeletonText,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
@@ -60,18 +64,19 @@ const ConferencingLayout = () => {
|
|||||||
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
||||||
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
|
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
|
||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading)
|
||||||
|
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
||||||
<Meta title="Conferencing" description="Add your favourite video conferencing apps for your meetings" />
|
<Meta title={t("conferencing")} description={t("conferencing_description")} />
|
||||||
<List>
|
<List>
|
||||||
{apps?.items &&
|
{apps?.items &&
|
||||||
apps.items
|
apps.items
|
||||||
.map((app) => ({ ...app, title: app.title || app.name }))
|
.map((app) => ({ ...app, title: app.title || app.name }))
|
||||||
.map((app) => (
|
.map((app) => (
|
||||||
<ListItem className="flex-col border-0" key={app.title}>
|
<ListItem className="flex-col border-0" key={app.title}>
|
||||||
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse">
|
<div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse">
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
|
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
|
||||||
@@ -128,4 +133,14 @@ const ConferencingLayout = () => {
|
|||||||
|
|
||||||
ConferencingLayout.getLayout = getLayout;
|
ConferencingLayout.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ConferencingLayout;
|
export default ConferencingLayout;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
@@ -21,9 +22,12 @@ import {
|
|||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
import { nameOfDay } from "@lib/core/i18n/weekday";
|
import { nameOfDay } from "@lib/core/i18n/weekday";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
@@ -47,14 +51,14 @@ const GeneralQueryView = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("something_went_wrong"));
|
throw new Error(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<WithQuery
|
<WithQuery
|
||||||
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -66,6 +70,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
reset(getValues());
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -115,7 +120,12 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const {
|
||||||
|
formState: { isDirty, isSubmitting },
|
||||||
|
reset,
|
||||||
|
getValues,
|
||||||
|
} = formMethods;
|
||||||
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
@@ -127,7 +137,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
weekStart: values.weekStart.value,
|
weekStart: values.weekStart.value,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<Meta title="General" description="Manage settings for your language and timezone" />
|
<Meta title={t("general")} description={t("general_description")} />
|
||||||
<Controller
|
<Controller
|
||||||
name="locale"
|
name="locale"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
@@ -156,7 +166,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
id="timezone"
|
id="timezone"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event) formMethods.setValue("timeZone", event.value);
|
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -174,7 +184,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
value={value}
|
value={value}
|
||||||
options={timeFormatOptions}
|
options={timeFormatOptions}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event) formMethods.setValue("timeFormat", { ...event });
|
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -195,13 +205,13 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
value={value}
|
value={value}
|
||||||
options={weekStartOptions}
|
options={weekStartOptions}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event) formMethods.setValue("weekStart", { ...event });
|
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button color="primary" type="submit" className="mt-8">
|
<Button disabled={isDisabled} color="primary" type="submit" className="mt-8">
|
||||||
<>{t("update")}</>
|
<>{t("update")}</>
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -210,4 +220,14 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
|
|
||||||
GeneralQueryView.getLayout = getLayout;
|
GeneralQueryView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default GeneralQueryView;
|
export default GeneralQueryView;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
|
import { BaseSyntheticEvent, useRef, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
@@ -35,11 +36,14 @@ import {
|
|||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<SkeletonAvatar className=" h-12 w-12 px-4" />
|
<SkeletonAvatar className=" h-12 w-12 px-4" />
|
||||||
@@ -59,14 +63,25 @@ interface DeleteAccountValues {
|
|||||||
totpCode: string;
|
totpCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
bio: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ProfileView = () => {
|
const ProfileView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
|
const { data: avatar, isLoading: isLoadingAvatar } = trpc.viewer.avatar.useQuery();
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
|
utils.viewer.me.invalidate();
|
||||||
|
utils.viewer.avatar.invalidate();
|
||||||
|
setTempFormValues(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showToast(t("error_updating_settings"), "error");
|
showToast(t("error_updating_settings"), "error");
|
||||||
@@ -74,18 +89,14 @@ const ProfileView = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
||||||
|
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
|
||||||
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
||||||
|
|
||||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||||
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
||||||
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
||||||
|
|
||||||
const form = useForm<DeleteAccountValues>();
|
const form = useForm<DeleteAccountValues>();
|
||||||
|
|
||||||
const emailMd5 = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(user?.email || "example@example.com")
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const onDeleteMeSuccessMutation = async () => {
|
const onDeleteMeSuccessMutation = async () => {
|
||||||
await utils.viewer.me.invalidate();
|
await utils.viewer.me.invalidate();
|
||||||
showToast(t("Your account was deleted"), "success");
|
showToast(t("Your account was deleted"), "success");
|
||||||
@@ -100,7 +111,7 @@ const ProfileView = () => {
|
|||||||
|
|
||||||
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
|
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
mutation.mutate(formMethods.getValues());
|
if (tempFormValues) mutation.mutate(tempFormValues);
|
||||||
setConfirmPasswordOpen(false);
|
setConfirmPasswordOpen(false);
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
@@ -146,6 +157,7 @@ const ProfileView = () => {
|
|||||||
deleteMeWithoutPasswordMutation.mutate();
|
deleteMeWithoutPasswordMutation.mutate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
|
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (isCALIdentityProviver) {
|
if (isCALIdentityProviver) {
|
||||||
@@ -156,38 +168,6 @@ const ProfileView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formMethods = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
avatar: "",
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
bio: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting, isDirty },
|
|
||||||
} = formMethods;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
reset(
|
|
||||||
{
|
|
||||||
avatar: user?.avatar || "",
|
|
||||||
username: user?.username || "",
|
|
||||||
name: user?.name || "",
|
|
||||||
email: user?.email || "",
|
|
||||||
bio: user?.bio || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepDirtyValues: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [reset, user]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const passwordRef = useRef<HTMLInputElement>(null!);
|
const passwordRef = useRef<HTMLInputElement>(null!);
|
||||||
|
|
||||||
@@ -199,143 +179,100 @@ const ProfileView = () => {
|
|||||||
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
||||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||||
};
|
};
|
||||||
const onSuccessfulUsernameUpdate = async () => {
|
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
|
||||||
await utils.viewer.me.invalidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onErrorInUsernameUpdate = () => {
|
if (isLoading || !user || isLoadingAvatar || !avatar)
|
||||||
showToast(t("error_updating_settings"), "error");
|
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading || !user) return <SkeletonLoader />;
|
const defaultValues = {
|
||||||
const isDisabled = isSubmitting || !isDirty;
|
username: user.username || "",
|
||||||
|
avatar: avatar.avatar || "",
|
||||||
|
name: user.name || "",
|
||||||
|
email: user.email || "",
|
||||||
|
bio: user.bio || "",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form
|
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||||
form={formMethods}
|
<ProfileForm
|
||||||
handleSubmit={(values) => {
|
key={JSON.stringify(defaultValues)}
|
||||||
if (values.email !== user?.email && isCALIdentityProviver) {
|
defaultValues={defaultValues}
|
||||||
|
onSubmit={(values) => {
|
||||||
|
if (values.email !== user.email && isCALIdentityProviver) {
|
||||||
|
setTempFormValues(values);
|
||||||
setConfirmPasswordOpen(true);
|
setConfirmPasswordOpen(true);
|
||||||
} else {
|
} else {
|
||||||
mutation.mutate(values);
|
mutation.mutate(values);
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
<Meta title="Profile" description={t("profile_description", { appName: APP_NAME })} />
|
extraField={
|
||||||
<div className="flex items-center">
|
<div className="mt-8">
|
||||||
<Controller
|
<UsernameAvailabilityField
|
||||||
control={formMethods.control}
|
user={user}
|
||||||
name="avatar"
|
onSuccessMutation={async () => {
|
||||||
render={({ field: { value } }) => (
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
<>
|
await utils.viewer.me.invalidate();
|
||||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
}}
|
||||||
<div className="ml-4">
|
onErrorMutation={() => {
|
||||||
<ImageUploader
|
showToast(t("error_updating_settings"), "error");
|
||||||
target="avatar"
|
}}
|
||||||
id="avatar-upload"
|
/>
|
||||||
buttonMsg={t("change_avatar")}
|
</div>
|
||||||
handleAvatarChange={(newAvatar) => {
|
}
|
||||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
/>
|
||||||
}}
|
|
||||||
imageSrc={value}
|
<hr className="my-6 border-neutral-200" />
|
||||||
/>
|
|
||||||
</div>
|
<Label>{t("danger_zone")}</Label>
|
||||||
</>
|
{/* Delete account Dialog */}
|
||||||
|
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
data-testid="delete-account"
|
||||||
|
color="destructive"
|
||||||
|
className="mt-1 border-2"
|
||||||
|
StartIcon={Icon.FiTrash2}>
|
||||||
|
{t("delete_account")}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
title={t("delete_account_modal_title")}
|
||||||
|
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||||
|
type="creation"
|
||||||
|
Icon={Icon.FiAlertTriangle}>
|
||||||
|
<>
|
||||||
|
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
|
||||||
|
{isCALIdentityProviver && (
|
||||||
|
<PasswordField
|
||||||
|
data-testid="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
label="Password"
|
||||||
|
ref={passwordRef}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<Controller
|
|
||||||
control={formMethods.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field: { ref, onChange, value } }) => {
|
|
||||||
return (
|
|
||||||
<UsernameAvailability
|
|
||||||
currentUsername={user?.username || ""}
|
|
||||||
inputUsernameValue={value}
|
|
||||||
usernameRef={ref}
|
|
||||||
setInputUsernameValue={onChange}
|
|
||||||
onSuccessMutation={onSuccessfulUsernameUpdate}
|
|
||||||
onErrorMutation={onErrorInUsernameUpdate}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
||||||
disabled={isDisabled}
|
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
||||||
color="primary"
|
<TwoFactor center={false} />
|
||||||
className="mt-8"
|
</Form>
|
||||||
type="submit"
|
)}
|
||||||
loading={mutation.isLoading}>
|
|
||||||
{t("update")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<hr className="my-6 border-neutral-200" />
|
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
||||||
|
<DialogFooter>
|
||||||
<Label>{t("danger_zone")}</Label>
|
<Button
|
||||||
{/* Delete account Dialog */}
|
color="primary"
|
||||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
data-testid="delete-account-confirm"
|
||||||
<DialogTrigger asChild>
|
onClick={(e) => onConfirmButton(e)}>
|
||||||
<Button
|
{t("delete_my_account")}
|
||||||
data-testid="delete-account"
|
</Button>
|
||||||
color="destructive"
|
<DialogClose />
|
||||||
className="mt-1 border-2"
|
</DialogFooter>
|
||||||
StartIcon={Icon.FiTrash2}>
|
</>
|
||||||
{t("delete_account")}
|
</DialogContent>
|
||||||
</Button>
|
</Dialog>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
title={t("delete_account_modal_title")}
|
|
||||||
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
|
||||||
type="creation"
|
|
||||||
Icon={Icon.FiAlertTriangle}>
|
|
||||||
<>
|
|
||||||
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
|
|
||||||
{isCALIdentityProviver && (
|
|
||||||
<PasswordField
|
|
||||||
data-testid="password"
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
label="Password"
|
|
||||||
ref={passwordRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
|
||||||
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
|
||||||
<TwoFactor center={false} />
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
data-testid="delete-account-confirm"
|
|
||||||
onClick={(e) => onConfirmButton(e)}>
|
|
||||||
{t("delete_my_account")}
|
|
||||||
</Button>
|
|
||||||
<DialogClose />
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* If changing email, confirm password */}
|
{/* If changing email, confirm password */}
|
||||||
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
||||||
@@ -369,6 +306,83 @@ const ProfileView = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ProfileForm = ({
|
||||||
|
defaultValues,
|
||||||
|
onSubmit,
|
||||||
|
extraField,
|
||||||
|
}: {
|
||||||
|
defaultValues: FormValues;
|
||||||
|
onSubmit: (values: FormValues) => void;
|
||||||
|
extraField?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const emailMd5 = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(defaultValues.email || "example@example.com")
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const formMethods = useForm<FormValues>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = formMethods;
|
||||||
|
|
||||||
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Controller
|
||||||
|
control={formMethods.control}
|
||||||
|
name="avatar"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<>
|
||||||
|
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<ImageUploader
|
||||||
|
target="avatar"
|
||||||
|
id="avatar-upload"
|
||||||
|
buttonMsg={t("change_avatar")}
|
||||||
|
handleAvatarChange={(newAvatar) => {
|
||||||
|
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
imageSrc={value || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{extraField}
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ProfileView.getLayout = getLayout;
|
ProfileView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileView;
|
export default ProfileView;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -13,6 +14,8 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const ProfileImpersonationView = () => {
|
const ProfileImpersonationView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
@@ -20,8 +23,24 @@ const ProfileImpersonationView = () => {
|
|||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast(t("profile_updated_successfully"), "success");
|
showToast(t("profile_updated_successfully"), "success");
|
||||||
|
reset(getValues());
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onSettled: () => {
|
||||||
|
utils.viewer.me.invalidate();
|
||||||
|
},
|
||||||
|
onMutate: async ({ disableImpersonation }) => {
|
||||||
|
await utils.viewer.me.cancel();
|
||||||
|
const previousValue = utils.viewer.me.getData();
|
||||||
|
|
||||||
|
if (previousValue && disableImpersonation) {
|
||||||
|
utils.viewer.me.setData(undefined, { ...previousValue, disableImpersonation });
|
||||||
|
}
|
||||||
|
return { previousValue };
|
||||||
|
},
|
||||||
|
onError: (error, variables, context) => {
|
||||||
|
if (context?.previousValue) {
|
||||||
|
utils.viewer.me.setData(undefined, context.previousValue);
|
||||||
|
}
|
||||||
showToast(`${t("error")}, ${error.message}`, "error");
|
showToast(`${t("error")}, ${error.message}`, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -33,25 +52,26 @@ const ProfileImpersonationView = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting, isDirty },
|
||||||
setValue,
|
setValue,
|
||||||
|
reset,
|
||||||
|
getValues,
|
||||||
} = formMethods;
|
} = formMethods;
|
||||||
|
|
||||||
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Impersonation Settings" description="" />
|
<Meta title={t("impersonation")} description={t("impersonation_description")} />
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={({ disableImpersonation }) => {
|
handleSubmit={({ disableImpersonation }) => {
|
||||||
mutation.mutate({ disableImpersonation });
|
mutation.mutate({ disableImpersonation });
|
||||||
utils.viewer.me.invalidate();
|
|
||||||
}}>
|
}}>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Switch
|
<Switch
|
||||||
{...formMethods.register("disableImpersonation")}
|
|
||||||
defaultChecked={!user?.disableImpersonation}
|
defaultChecked={!user?.disableImpersonation}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
setValue("disableImpersonation", !e);
|
setValue("disableImpersonation", !e, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
fitToHeight={true}
|
fitToHeight={true}
|
||||||
/>
|
/>
|
||||||
@@ -64,7 +84,7 @@ const ProfileImpersonationView = () => {
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" className="mt-8" type="submit" disabled={isSubmitting || mutation.isLoading}>
|
<Button color="primary" className="mt-8" type="submit" disabled={isDisabled}>
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -74,4 +94,14 @@ const ProfileImpersonationView = () => {
|
|||||||
|
|
||||||
ProfileImpersonationView.getLayout = getLayout;
|
ProfileImpersonationView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
await ssr.viewer.me.prefetch();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileImpersonationView;
|
export default ProfileImpersonationView;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
|
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
type ChangePasswordFormValues = {
|
type ChangePasswordFormValues = {
|
||||||
oldPassword: string;
|
oldPassword: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
@@ -92,4 +95,14 @@ const PasswordView = () => {
|
|||||||
|
|
||||||
PasswordView.getLayout = getLayout;
|
PasswordView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default PasswordView;
|
export default PasswordView;
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,12 +1,36 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, getSettingsLayout as getLayout, Loader, Meta, Switch } from "@calcom/ui";
|
import {
|
||||||
|
Badge,
|
||||||
|
getSettingsLayout as getLayout,
|
||||||
|
Meta,
|
||||||
|
Switch,
|
||||||
|
SkeletonButton,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = () => {
|
||||||
|
return (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TwoFactorAuthView = () => {
|
const TwoFactorAuthView = () => {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
@@ -16,11 +40,11 @@ const TwoFactorAuthView = () => {
|
|||||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||||
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <SkeletonLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Two-Factor Authentication" description="Manage settings for your account passwords" />
|
<Meta title={t("2fa")} description={t("2fa_description")} />
|
||||||
<div className="mt-6 flex items-start space-x-4">
|
<div className="mt-6 flex items-start space-x-4">
|
||||||
<Switch
|
<Switch
|
||||||
checked={user?.twoFactorEnabled}
|
checked={user?.twoFactorEnabled}
|
||||||
@@ -35,7 +59,7 @@ const TwoFactorAuthView = () => {
|
|||||||
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">Add an extra layer of security to your account.</p>
|
<p className="text-sm text-gray-600">{t("add_an_extra_layer_of_security")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,4 +92,14 @@ const TwoFactorAuthView = () => {
|
|||||||
|
|
||||||
TwoFactorAuthView.getLayout = getLayout;
|
TwoFactorAuthView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default TwoFactorAuthView;
|
export default TwoFactorAuthView;
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getWizardLayout as getLayout } from "@calcom/ui";
|
import { getWizardLayout as getLayout } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const CreateNewTeamPage = () => {
|
const CreateNewTeamPage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create a new Team</title>
|
<title>{t("create_new_team")}</title>
|
||||||
<meta name="description" content="Create a new team to ease your organisational booking" />
|
<meta name="description" content={t("create_new_team_description")} />
|
||||||
</Head>
|
</Head>
|
||||||
<CreateANewTeamForm />
|
<CreateANewTeamForm />
|
||||||
</>
|
</>
|
||||||
@@ -17,4 +22,14 @@ const CreateNewTeamPage = () => {
|
|||||||
|
|
||||||
CreateNewTeamPage.getLayout = getLayout;
|
CreateNewTeamPage.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default CreateNewTeamPage;
|
export default CreateNewTeamPage;
|
||||||
@@ -6,6 +6,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { Alert, Button, EmailField, PasswordField, TextField } from "@calcom/ui";
|
import { Alert, Button, EmailField, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
|
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
|
||||||
@@ -26,6 +27,8 @@ type FormValues = {
|
|||||||
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
|
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const methods = useForm<FormValues>({
|
const methods = useForm<FormValues>({
|
||||||
defaultValues: prepopulateFormValues,
|
defaultValues: prepopulateFormValues,
|
||||||
});
|
});
|
||||||
@@ -53,8 +56,12 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
})
|
})
|
||||||
.then(handleErrors)
|
.then(handleErrors)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await signIn("Cal.com", {
|
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
||||||
callbackUrl: router.query.callbackUrl ? `${WEBAPP_URL}/${router.query.callbackUrl}` : WEBAPP_URL,
|
await signIn<"credentials">("credentials", {
|
||||||
|
...data,
|
||||||
|
callbackUrl: router.query.callbackUrl
|
||||||
|
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||||
|
: `${WEBAPP_URL}/getting-started`,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -111,7 +118,9 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
className="w-5/12 justify-center"
|
className="w-5/12 justify-center"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
signIn("Cal.com", {
|
signIn("Cal.com", {
|
||||||
callbackUrl: (`${WEBAPP_URL}/${router.query.callbackUrl}` || "") as string,
|
callbackUrl: router.query.callbackUrl
|
||||||
|
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||||
|
: `${WEBAPP_URL}/getting-started`,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
{t("login_instead")}
|
{t("login_instead")}
|
||||||
|
|||||||
Loaded 100 of 553 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user