Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1929d5c7d4 | ||
|
|
8ae54cd7cc | ||
|
|
bb13009e64 | ||
|
|
648dd862f5 | ||
|
|
8eccd3658e | ||
|
|
ed65b2a3ab | ||
|
|
0930121680 | ||
|
|
1068873f27 | ||
|
|
a1ece0be63 | ||
|
|
c550b4c8fc | ||
|
|
e95650637c | ||
|
|
0f3e0a1554 | ||
|
|
761dfac033 | ||
|
|
9fda4acc5b | ||
|
|
0470708aad | ||
|
|
bee392ba1a | ||
|
|
448f57442b | ||
|
|
856b090b4b | ||
|
|
b99661a411 | ||
|
|
d96a26eb3c | ||
|
|
9d08f5c7c8 | ||
|
|
4fe5f6ca6f | ||
|
|
af505935bd | ||
|
|
4a30f60622 | ||
|
|
24ed04e6c7 | ||
|
|
bed3595314 | ||
|
|
74671263bf | ||
|
|
bdc5e9c7e9 | ||
|
|
8fa1c70a97 | ||
|
|
1343c8fa8d | ||
|
|
5c0f9510a1 | ||
|
|
b2801170fa | ||
|
|
6ac19e8307 | ||
|
|
344241cfbf | ||
|
|
66ebf18fa9 | ||
|
|
e6d6f3317e | ||
|
|
55f66e6bcd | ||
|
|
22e2b0a6a9 | ||
|
|
e24f3828cb | ||
|
|
70f5bdc499 | ||
|
|
8731a39c3c | ||
|
|
e2d9e75475 | ||
|
|
55c7707b94 | ||
|
|
98944d4815 | ||
|
|
faba25acbd | ||
|
|
aab5ee344c | ||
|
|
6880f88dbe | ||
|
|
9177fe86dc | ||
|
|
bf3f8acbc0 | ||
|
|
b4fa9826f2 | ||
|
|
955de3133c | ||
|
|
faac3d0f57 | ||
|
|
3fb1ede770 | ||
|
|
53224886e3 | ||
|
|
e513180d7e | ||
|
|
3116d6df1a | ||
|
|
4284a4d16d | ||
|
|
eadca3b2c0 | ||
|
|
68125af29f | ||
|
|
d436e628fe | ||
|
|
c5483c81de | ||
|
|
9db4a04cd9 | ||
|
|
8943e8c2f6 | ||
|
|
f17fa55426 | ||
|
|
9ff83075d6 | ||
|
|
6b6043afdc | ||
|
|
fd9162e70f | ||
|
|
8331059b7c | ||
|
|
f0992b4914 | ||
|
|
e081a4cfc2 | ||
|
|
895b3ca7c9 | ||
|
|
4d30264950 | ||
|
|
3a352a94f6 | ||
|
|
8623175612 | ||
|
|
ea531777c4 | ||
|
|
2639ef7e13 | ||
|
|
0e76a19c96 | ||
|
|
3ec9eb377d | ||
|
|
44b7000509 | ||
|
|
251307f1e6 | ||
|
|
75e638ec4d | ||
|
|
443ccdd6c0 | ||
|
|
dbf11cc04b | ||
|
|
a5c9729f5f | ||
|
|
a6e2983a22 | ||
|
|
a21f0c46c7 | ||
|
|
ed4a81e852 | ||
|
|
9231218350 | ||
|
|
7d1ea4ee2b | ||
|
|
0c68e51930 | ||
|
|
8e565dc610 | ||
|
|
5b47844fa1 | ||
|
|
0d01d3246a | ||
|
|
bb31c4ba14 | ||
|
|
d9a7952467 | ||
|
|
208dcbbda3 | ||
|
|
ff4494d395 | ||
|
|
db4b32119e | ||
|
|
bfef5b1877 | ||
|
|
91962cc90b | ||
|
|
7a71a5ddd4 | ||
|
|
ca47a589ba | ||
|
|
a33dbd8922 | ||
|
|
aaab4ca741 | ||
|
|
6aadf7429d | ||
|
|
748307bc91 | ||
|
|
559389a319 | ||
|
|
a9140b6a76 | ||
|
|
8f50e873c9 | ||
|
|
9b28b7f78a | ||
|
|
4417097f97 | ||
|
|
93fc036d2c | ||
|
|
7f13fc58b9 | ||
|
|
527d1f6c93 | ||
|
|
0ec3c496d8 | ||
|
|
62ec3de0b1 | ||
|
|
2c47cb6a3b | ||
|
|
dc2b145843 | ||
|
|
c958e1578a | ||
|
|
1e857e5c0a | ||
|
|
9bc8d48415 | ||
|
|
f86c6734a7 | ||
|
|
3d5ff82062 | ||
|
|
8b63483e2e | ||
|
|
c97d4288c0 | ||
|
|
ff75364f22 | ||
|
|
7f1b028099 | ||
|
|
9e70a985e4 | ||
|
|
057cf273dd | ||
|
|
9af621294d | ||
|
|
bfb5a31032 | ||
|
|
6308955e89 | ||
|
|
b0c5b629d7 | ||
|
|
10f965570b | ||
|
|
c24f5200f5 | ||
|
|
f922692a1a | ||
|
|
65c137f67d | ||
|
|
585679969a | ||
|
|
893b67b6c4 | ||
|
|
3c296b440e | ||
|
|
f8a59b52f7 | ||
|
|
17d106cfda | ||
|
|
dcf9d30382 | ||
|
|
eafd7f6b77 | ||
|
|
f31165b442 | ||
|
|
40c5d5871e | ||
|
|
2362754356 | ||
|
|
8ccf3b589e | ||
|
|
2dbc73c75b | ||
|
|
87e1ad7115 | ||
|
|
2e9895aceb | ||
|
|
fbbc573f33 | ||
|
|
8bc8a045b6 | ||
|
|
8c049b29b3 | ||
|
|
f1e783f047 | ||
|
|
3baa6c7145 | ||
|
|
dd28bb46db | ||
|
|
c182091fa9 | ||
|
|
d061572ed6 | ||
|
|
3a943fbc81 | ||
|
|
8a58a51a2c | ||
|
|
e92265ceaa | ||
|
|
b7b1fff1c0 | ||
|
|
886f7021f4 | ||
|
|
2b17188087 | ||
|
|
d498988bd4 | ||
|
|
785a08ba2c | ||
|
|
6aa83102e4 | ||
|
|
97fe41f3ba | ||
|
|
b7cecfbb66 | ||
|
|
7f7d80e653 | ||
|
|
64796a96fc | ||
|
|
83cc7dc86e | ||
|
|
75c9145558 | ||
|
|
869f459d98 | ||
|
|
e00008b663 | ||
|
|
175f2482e1 | ||
|
|
9d91d72a72 | ||
|
|
fd9cd1b98b | ||
|
|
2640040359 | ||
|
|
dec8f8e896 | ||
|
|
b4eff25636 | ||
|
|
2a752e5358 | ||
|
|
b2684837c1 | ||
|
|
2fed2133a5 | ||
|
|
7fc0877c26 | ||
|
|
f7ce774ddc | ||
|
|
174c730f1f | ||
|
|
8ad513e4ec | ||
|
|
a0bf5b4067 | ||
|
|
484f603c9e | ||
|
|
94b16fb327 | ||
|
|
55db7bdf76 | ||
|
|
14af241d5f | ||
|
|
4efa90e42f | ||
|
|
581e1759c4 | ||
|
|
61cdac321e | ||
|
|
59a04137a1 | ||
|
|
eba1a1ffff | ||
|
|
df25b179d7 | ||
|
|
ff2d9ad67d | ||
|
|
485b72cd0d | ||
|
|
57eb5a4328 | ||
|
|
2c4954e31c | ||
|
|
748f2b42e8 | ||
|
|
6bc713bc2e | ||
|
|
b1019d6bab | ||
|
|
268f1872e4 | ||
|
|
60d439cc32 | ||
|
|
36d201ac20 | ||
|
|
d6644bc232 | ||
|
|
7a9b893931 | ||
|
|
235c314e68 |
No files matched your search
@@ -106,12 +106,6 @@ LARK_OPEN_APP_ID=""
|
|||||||
LARK_OPEN_APP_SECRET=""
|
LARK_OPEN_APP_SECRET=""
|
||||||
LARK_OPEN_VERIFICATION_TOKEN=""
|
LARK_OPEN_VERIFICATION_TOKEN=""
|
||||||
|
|
||||||
# - WEB3
|
|
||||||
# Used for the Web3 plugin
|
|
||||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
|
|
||||||
ALCHEMY_API_KEY=""
|
|
||||||
INFURA_API_KEY=""
|
|
||||||
|
|
||||||
# - SALESFORCE
|
# - SALESFORCE
|
||||||
# Used for the Salesforce (Sales Cloud) app
|
# Used for the Salesforce (Sales Cloud) app
|
||||||
SALESFORCE_CONSUMER_KEY=""
|
SALESFORCE_CONSUMER_KEY=""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# - SHARED
|
# - SHARED
|
||||||
# - NEXTAUTH
|
# - NEXTAUTH
|
||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
# - ORGANIZATIONS
|
||||||
|
|
||||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||||
@@ -22,6 +23,8 @@ CALCOM_LICENSE_KEY=
|
|||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
|
UPSTASH_REDIS_REST_URL=
|
||||||
|
UPSTASH_REDIS_REST_TOKEN=
|
||||||
|
|
||||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
@@ -32,6 +35,8 @@ PRISMA_GENERATE_DATAPROXY=
|
|||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
|
# Set this to http://app.cal.local:3000 if you want to enable organizations, and
|
||||||
|
# check variable ORGANIZATIONS_ENABLED at the bottom of this file
|
||||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||||
@@ -183,3 +188,26 @@ CSP_POLICY=
|
|||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
|
|
||||||
|
# - ORGANIZATIONS *******************************************************************************************
|
||||||
|
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
||||||
|
# This is mainly needed locally, because for orgs to work a full domain name needs to point
|
||||||
|
# to the app, i.e. app.cal.local instead of using localhost, which is very disruptive
|
||||||
|
#
|
||||||
|
# This variable should only be set to 1 or true if you are in a non-prod environment and you want to
|
||||||
|
# use organizations
|
||||||
|
ORGANIZATIONS_ENABLED=
|
||||||
|
|
||||||
|
# Vercel Config to create subdomains for organizations
|
||||||
|
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
|
||||||
|
PROJECT_ID_VERCEL=
|
||||||
|
# Get it from: https://vercel.com/teams/<TEAM_SLUG>/settings
|
||||||
|
TEAM_ID_VERCEL=
|
||||||
|
# Get it from: https://vercel.com/account/tokens
|
||||||
|
AUTH_BEARER_TOKEN_VERCEL=
|
||||||
|
|
||||||
|
#Enables New booker for Embed only
|
||||||
|
NEW_BOOKER_ENABLED_FOR_EMBED=0
|
||||||
|
|
||||||
|
#Enables New booker for All but Embed requests
|
||||||
|
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||||
@@ -8,8 +8,6 @@ Fixes # (issue)
|
|||||||
Loom Video: https://www.loom.com/
|
Loom Video: https://www.loom.com/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Environment**: Staging(main branch) / Production
|
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete bullets that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
@@ -27,13 +25,14 @@ Fixes # (issue)
|
|||||||
- [ ] Test A
|
- [ ] Test A
|
||||||
- [ ] Test B
|
- [ ] Test B
|
||||||
|
|
||||||
|
## Mandatory Tasks
|
||||||
|
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
<!-- Please remove all the irrelevant bullets to your PR -->
|
||||||
|
|
||||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||||
- My code doesn't follow the style guidelines of this project
|
- My code doesn't follow the style guidelines of this project
|
||||||
- I haven't performed a self-review of my own code and corrected any misspellings
|
|
||||||
- I haven't commented my code, particularly in hard-to-understand areas
|
- I haven't commented my code, particularly in hard-to-understand areas
|
||||||
- I haven't checked if my PR needs changes to the documentation
|
- I haven't checked if my PR needs changes to the documentation
|
||||||
- I haven't checked if my changes generate no new warnings
|
- I haven't checked if my changes generate no new warnings
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ runs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
yarn-download-cache-
|
yarn-download-cache-
|
||||||
|
|
||||||
|
# Invalidated on yarn.lock changes
|
||||||
|
- name: Restore node_modules
|
||||||
|
id: yarn-nm-cache
|
||||||
|
uses: buildjet/cache@v3
|
||||||
|
with:
|
||||||
|
path: "**/node_modules/"
|
||||||
|
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||||
|
|
||||||
# Invalidated on yarn.lock changes
|
# Invalidated on yarn.lock changes
|
||||||
- name: Restore yarn install state
|
- name: Restore yarn install state
|
||||||
id: yarn-install-state-cache
|
id: yarn-install-state-cache
|
||||||
@@ -58,5 +66,6 @@ runs:
|
|||||||
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
||||||
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
||||||
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
||||||
|
YARN_NM_MODE: "hardlinks-local" # Reduce node_modules size
|
||||||
# Other environment variables
|
# Other environment variables
|
||||||
HUSKY: "0" # By default do not run HUSKY install
|
HUSKY: "0" # By default do not run HUSKY install
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Add PRs to project Reviewing PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-PR-to-project:
|
||||||
|
name: Add PRs to project Reviewing PRs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v0.1.0
|
||||||
|
with:
|
||||||
|
project-url: https://github.com/orgs/calcom/projects/11
|
||||||
|
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
@@ -1,27 +1,75 @@
|
|||||||
name: "Apply issue labels to PR"
|
name: "Apply issue labels to PR"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened]
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label_on_pr:
|
label_on_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: none
|
||||||
|
issues: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
- name: Apply labels from linked issue to PR
|
- name: Apply labels from linked issue to PR
|
||||||
env:
|
uses: actions/github-script@v5
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
with:
|
||||||
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
script: |
|
||||||
|
async function getLinkedIssues(owner, repo, prNumber) {
|
||||||
|
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
prNumber: prNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await github.graphql(query, variables);
|
||||||
|
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
const linkedIssues = await getLinkedIssues(
|
||||||
|
context.repo.owner,
|
||||||
|
context.repo.repo,
|
||||||
|
pr.number
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelsToAdd = new Set();
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
if (issue.labels && issue.labels.nodes) {
|
||||||
|
for (const label of issue.labels.nodes) {
|
||||||
|
labelsToAdd.add(label.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelsToAdd.size) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
labels: Array.from(labelsToAdd),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -25,7 +25,9 @@ jobs:
|
|||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test-e2e:embed-react
|
run: |
|
||||||
|
yarn test-e2e:embed-react
|
||||||
|
yarn workspace @calcom/embed-react packaged:tests
|
||||||
env:
|
env:
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- name: Save Code Linting Reports
|
- name: Run Linting with Reports
|
||||||
run: yarn lint:report
|
run: yarn lint:report
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
|
name: Detect changes
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
const https = require("https");
|
|
||||||
|
|
||||||
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|
||||||
// Get the labels from issues linked to the PR
|
|
||||||
const query = `
|
|
||||||
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
pullRequest(number: $prNumber) {
|
|
||||||
closingIssuesReferences(first: 10) {
|
|
||||||
nodes {
|
|
||||||
number
|
|
||||||
labels(first: 10) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const graphqlData = JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables: {
|
|
||||||
owner: pr.base.repo.owner.login,
|
|
||||||
repo: pr.base.repo.name,
|
|
||||||
prNumber: pr.number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
hostname: "api.github.com",
|
|
||||||
path: "/graphql",
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Content-Length": graphqlData.length,
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
"User-Agent": "Node.js",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the native Node.js request library to make the request
|
|
||||||
let linkedIssues;
|
|
||||||
linkedIssues = await new Promise((resolve, reject) => {
|
|
||||||
const request = https.request(requestOptions, (response) => {
|
|
||||||
let responseBody = "";
|
|
||||||
response.on("data", (chunk) => {
|
|
||||||
responseBody += chunk;
|
|
||||||
});
|
|
||||||
response.on("end", () => {
|
|
||||||
resolve(JSON.parse(responseBody)?.data?.repository?.pullRequest?.closingIssuesReferences?.nodes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.write(graphqlData);
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!linkedIssues || linkedIssues.length === 0) {
|
|
||||||
console.log("No issue linked.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over linked issues and apply labels to PR
|
|
||||||
for (const issue of linkedIssues) {
|
|
||||||
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
|
||||||
|
|
||||||
if (!labels || labels.length === 0) {
|
|
||||||
console.log(`No labels found on the linked issue #${issue.number}.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelsData = JSON.stringify({
|
|
||||||
labels: labels,
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
hostname: "api.github.com",
|
|
||||||
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Content-Length": labelsData.length,
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
"User-Agent": "Node.js",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let labelResult;
|
|
||||||
labelResult = await new Promise((resolve, reject) => {
|
|
||||||
const request = https.request(requestOptions, (response) => {
|
|
||||||
let responseBody = "";
|
|
||||||
response.on("data", (chunk) => {
|
|
||||||
responseBody += chunk;
|
|
||||||
});
|
|
||||||
response.on("data", () => {});
|
|
||||||
response.on("end", () => {
|
|
||||||
resolve(JSON.parse(responseBody));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on("error", (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.write(labelsData);
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (labelResult.message) {
|
|
||||||
console.log(`Error labelling PR: ${labelResult.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Applied labels: ${labels.join(", ")} to PR #${pr.number} from linked issue #${issue.number}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the PR data and GitHub token to the script
|
|
||||||
(async () => {
|
|
||||||
if (!process.env.PR_DATA) {
|
|
||||||
console.log("No PR data found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prData = JSON.parse(process.env.PR_DATA);
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
await applyLabelFromLinkedIssueToPR(prData, token);
|
|
||||||
})();
|
|
||||||
@@ -201,6 +201,14 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
|
In a development environment, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn workspace @calcom/prisma db-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
In a production environment, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn workspace @calcom/prisma db-deploy
|
yarn workspace @calcom/prisma db-deploy
|
||||||
```
|
```
|
||||||
@@ -416,7 +424,7 @@ Make sure to complete section "Obtaining the Google API Credentials". After the
|
|||||||
following
|
following
|
||||||
|
|
||||||
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
||||||
1. Under 'OAuth concent screen', click "PUBLISH APP"
|
1. Under 'OAuth consent screen', click "PUBLISH APP"
|
||||||
|
|
||||||
### Obtaining Microsoft Graph Client ID and Secret
|
### Obtaining Microsoft Graph Client ID and Secret
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import { availabilityUserSelect } from "@calcom/prisma";
|
import { availabilityUserSelect } from "@calcom/prisma";
|
||||||
@@ -119,8 +120,10 @@ const availabilitySchema = z
|
|||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { prisma, isAdmin, userId: reqUserId } = req;
|
const { prisma, isAdmin, userId: reqUserId } = req;
|
||||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
if (!teamId)
|
if (!teamId)
|
||||||
return getUserAvailability({
|
return getUserAvailability({
|
||||||
|
orgSlug: isValidOrgDomain ? currentOrgDomain || undefined : undefined,
|
||||||
username,
|
username,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
|||||||
* type: integer
|
* type: integer
|
||||||
* example: [2, 3, 4]
|
* example: [2, 3, 4]
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: attendeeEmails
|
* name: attendeeEmail
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* oneOf:
|
* oneOf:
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
* required:
|
* required:
|
||||||
* - eventTypeId
|
* - eventTypeId
|
||||||
* - start
|
* - start
|
||||||
* - end
|
|
||||||
* - name
|
* - name
|
||||||
* - email
|
* - email
|
||||||
* - timeZone
|
* - timeZone
|
||||||
|
|||||||
@@ -50,6 +50,6 @@
|
|||||||
"storybook-react-i18next": "^1.1.2",
|
"storybook-react-i18next": "^1.1.2",
|
||||||
"tailwindcss": "^3.2.1",
|
"tailwindcss": "^3.2.1",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^2.9.15"
|
"vite": "^4.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +235,7 @@
|
|||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand:#111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis:#101010;
|
||||||
|
--cal-brand-text: white;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
/* background */
|
/* background */
|
||||||
@@ -274,7 +275,8 @@
|
|||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand: #111827;
|
--cal-brand: white;
|
||||||
--cal-brand-emphasis: #101010;
|
--cal-brand-emphasis: #e1e1e1;
|
||||||
|
--cal-brand-text: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export const SnippedGenerator = {
|
|||||||
// extend some internals to gain information about current path, method and spec in the generator function metioned later
|
// extend some internals to gain information about current path, method and spec in the generator function metioned later
|
||||||
spec: {
|
spec: {
|
||||||
wrapSelectors: {
|
wrapSelectors: {
|
||||||
requestFor: (ori, system) => (state, path, method) => {
|
requestFor: (ori) => (state, path, method) => {
|
||||||
return ori(path, method)
|
return ori(path, method)
|
||||||
?.set("spec", state.get("json", {}))
|
?.set("spec", state.get("json", {}))
|
||||||
?.setIn(["oasPathMethod", "path"], path)
|
?.setIn(["oasPathMethod", "path"], path)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
timeoutRef.current = null;
|
timeoutRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import { createRef, forwardRef, useRef, useState } from "react";
|
|||||||
import type { ControlProps } from "react-select";
|
import type { ControlProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
|
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
|
||||||
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -41,12 +44,13 @@ type PreviewState = {
|
|||||||
height: string;
|
height: string;
|
||||||
};
|
};
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
floatingPopup: Record<string, string>;
|
floatingPopup: Record<string, string | boolean | undefined>;
|
||||||
elementClick: Record<string, string>;
|
elementClick: Record<string, string>;
|
||||||
palette: {
|
palette: {
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout: BookerLayouts;
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
@@ -129,11 +133,13 @@ const getEmbedUIInstructionString = ({
|
|||||||
theme,
|
theme,
|
||||||
brandColor,
|
brandColor,
|
||||||
hideEventTypeDetails,
|
hideEventTypeDetails,
|
||||||
|
layout,
|
||||||
}: {
|
}: {
|
||||||
apiName: string;
|
apiName: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout?: string;
|
||||||
}) => {
|
}) => {
|
||||||
theme = theme !== "auto" ? theme : undefined;
|
theme = theme !== "auto" ? theme : undefined;
|
||||||
return getInstructionString({
|
return getInstructionString({
|
||||||
@@ -147,6 +153,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
hideEventTypeDetails: hideEventTypeDetails,
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
|
layout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -260,6 +267,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: PreviewState["theme"];
|
theme: PreviewState["theme"];
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
hideEventTypeDetails: boolean;
|
hideEventTypeDetails: boolean;
|
||||||
|
layout?: BookerLayout;
|
||||||
};
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
@@ -267,6 +275,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
@@ -274,6 +283,7 @@ const getEmbedTypeSpecificString = ({
|
|||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!frameworkCodes[embedType]) {
|
if (!frameworkCodes[embedType]) {
|
||||||
@@ -626,6 +636,7 @@ const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme;
|
|||||||
const ChooseEmbedTypesDialogContent = () => {
|
const ChooseEmbedTypesDialogContent = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@@ -671,6 +682,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const flags = useFlagMap();
|
||||||
|
const isBookerLayoutsEnabled = flags["booker-layouts"] === true;
|
||||||
|
|
||||||
const s = (href: string) => {
|
const s = (href: string) => {
|
||||||
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||||
@@ -691,7 +704,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
const [previewState, setPreviewState] = useState({
|
const [previewState, setPreviewState] = useState<PreviewState>({
|
||||||
inline: {
|
inline: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@@ -703,6 +716,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
palette: {
|
palette: {
|
||||||
brandColor: "#000000",
|
brandColor: "#000000",
|
||||||
},
|
},
|
||||||
|
layout: BookerLayouts.MONTH_VIEW,
|
||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
@@ -765,6 +779,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
|
layout: previewState.layout,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
...previewState.palette,
|
...previewState.palette,
|
||||||
@@ -798,6 +813,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const layoutOptions = [
|
||||||
|
{ value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") },
|
||||||
|
{ value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") },
|
||||||
|
{ value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") },
|
||||||
|
];
|
||||||
|
|
||||||
const FloatingPopupPositionOptions = [
|
const FloatingPopupPositionOptions = [
|
||||||
{
|
{
|
||||||
value: "bottom-right",
|
value: "bottom-right",
|
||||||
@@ -1070,6 +1091,27 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
))}
|
))}
|
||||||
|
{isBookerLayoutsEnabled && (
|
||||||
|
<Label className="mb-6">
|
||||||
|
<div className="mb-2">{t("layout")}</div>
|
||||||
|
<Select
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={layoutOptions[0]}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
layout: option.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={layoutOptions}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -1133,12 +1175,7 @@ export const EmbedDialog = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const embedUrl: string = router.query.embedUrl as string;
|
const embedUrl: string = router.query.embedUrl as string;
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
||||||
name="embed"
|
|
||||||
clearQueryParamsOnClose={queryParamsForDialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) window.resetEmbedStatus();
|
|
||||||
}}>
|
|
||||||
{!router.query.embedType ? (
|
{!router.query.embedType ? (
|
||||||
<ChooseEmbedTypesDialogContent />
|
<ChooseEmbedTypesDialogContent />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import dynamic from "next/dynamic";
|
|
||||||
import type { Dispatch } from "react";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { JSONObject } from "superjson/dist/types";
|
|
||||||
|
|
||||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
|
||||||
|
|
||||||
export type GateState = {
|
|
||||||
rainbowToken?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GateProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
gates: Gate[];
|
|
||||||
appData: JSONObject;
|
|
||||||
dispatch: Dispatch<Partial<GateState>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
|
|
||||||
|
|
||||||
// To add a new Gate just add the gate logic to the switch statement
|
|
||||||
const Gates: React.FC<GateProps> = ({ children, gates, appData, dispatch }) => {
|
|
||||||
const [rainbowToken, setRainbowToken] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch({ rainbowToken });
|
|
||||||
}, [rainbowToken, dispatch]);
|
|
||||||
|
|
||||||
let gateWrappers = <>{children}</>;
|
|
||||||
|
|
||||||
// Recursively wraps the `gateWrappers` with new gates allowing for multiple gates
|
|
||||||
for (const gate of gates) {
|
|
||||||
switch (gate) {
|
|
||||||
case "rainbow":
|
|
||||||
if (appData.blockchainId && appData.smartContractAddress && !rainbowToken) {
|
|
||||||
gateWrappers = (
|
|
||||||
<RainbowGate
|
|
||||||
setToken={setRainbowToken}
|
|
||||||
chainId={appData.blockchainId as number}
|
|
||||||
tokenAddress={appData.smartContractAddress as string}>
|
|
||||||
{gateWrappers}
|
|
||||||
</RainbowGate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gateWrappers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Gates;
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
import AppProviders from "@lib/app-providers";
|
import AppProviders from "@lib/app-providers";
|
||||||
@@ -13,7 +16,7 @@ import { seoConfig } from "@lib/config/next-seo.config";
|
|||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
export interface CalPageWrapper {
|
export interface CalPageWrapper {
|
||||||
(props?: any): JSX.Element;
|
(props?: AppProps): JSX.Element;
|
||||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +52,30 @@ function PageWrapper(props: AppProps) {
|
|||||||
};
|
};
|
||||||
// Use the layout defined at the page level, if available
|
// Use the layout defined at the page level, if available
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
|
// Canonical: Check if the URL is from cal.com so that we set the canonical conditionally
|
||||||
|
const isCalcom =
|
||||||
|
WEBAPP_URL &&
|
||||||
|
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") || new URL(WEBAPP_URL).hostname.endsWith("cal.dev"));
|
||||||
|
const path = router.asPath;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProviders {...providerProps}>
|
<AppProviders {...providerProps}>
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
<Head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<DefaultSeo
|
||||||
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
|
canonical={
|
||||||
|
isCalcom
|
||||||
|
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
||||||
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
|
}
|
||||||
|
{...seoConfig.defaultNextSeo}
|
||||||
|
/>
|
||||||
<I18nLanguageHandler />
|
<I18nLanguageHandler />
|
||||||
<Script
|
<Script
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { FunctionComponent, SVGProps } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -55,7 +57,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={data.type}
|
type={data.type}
|
||||||
render={(installProps) => {
|
render={(installProps) => {
|
||||||
const props = { ...installProps } as any;
|
const props = { ...installProps } as FunctionComponent<SVGProps<SVGSVGElement>>;
|
||||||
return (
|
return (
|
||||||
<DropdownItem {...props} color="minimal" type="button">
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
<span className="flex items-center gap-x-2">
|
<span className="flex items-center gap-x-2">
|
||||||
|
|||||||
@@ -2,60 +2,12 @@ import type { ComponentProps } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
|
||||||
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
|
||||||
{
|
|
||||||
name: "calendar",
|
|
||||||
href: "/apps/installed/calendar",
|
|
||||||
icon: Calendar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "conferencing",
|
|
||||||
href: "/apps/installed/conferencing",
|
|
||||||
icon: Video,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "payment",
|
|
||||||
href: "/apps/installed/payment",
|
|
||||||
icon: CreditCard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "automation",
|
|
||||||
href: "/apps/installed/automation",
|
|
||||||
icon: Share2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "analytics",
|
|
||||||
href: "/apps/installed/analytics",
|
|
||||||
icon: BarChart,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "other",
|
|
||||||
href: "/apps/installed/other",
|
|
||||||
icon: Grid,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function InstalledAppsLayout({
|
export default function InstalledAppsLayout({
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
|
||||||
|
|
||||||
const query = trpc.viewer.integrations.useQuery({
|
|
||||||
variant,
|
|
||||||
onlyInstalled: true,
|
|
||||||
});
|
|
||||||
let actualTabs = tabs;
|
|
||||||
if (query.data?.items.length === 0) {
|
|
||||||
actualTabs = tabs.filter((tab) => tab.name !== variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} hideHeadingOnMobile>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||||
|
|||||||
@@ -26,24 +26,24 @@ export default function TwoFactor({ center = true }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4">{t("2fa_code")}</Label>
|
||||||
|
|
||||||
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
|
||||||
|
<input type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
|
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<Input
|
{digits.map((digit, index) => (
|
||||||
className={className}
|
<Input
|
||||||
name="2fa1"
|
key={`2fa${index}`}
|
||||||
inputMode="decimal"
|
className={className}
|
||||||
{...digits[0]}
|
name={`2fa${index + 1}`}
|
||||||
autoFocus
|
inputMode="decimal"
|
||||||
autoComplete="one-time-code"
|
{...digit}
|
||||||
/>
|
autoFocus={index === 0}
|
||||||
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
|
autoComplete="one-time-code"
|
||||||
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
|
/>
|
||||||
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />
|
))}
|
||||||
<Input className={className} name="2fa5" inputMode="decimal" {...digits[4]} />
|
|
||||||
<Input className={className} name="2fa6" inputMode="decimal" {...digits[5]} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
}
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"in_person_attendee_address",
|
||||||
"in_person",
|
"in_person",
|
||||||
"attendee_phone_number",
|
"attendee_phone_number",
|
||||||
"link_meeting",
|
"link_meeting",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ type AvailableTimesProps = {
|
|||||||
bookingAttendees?: number | null;
|
bookingAttendees?: number | null;
|
||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
ethSignature?: string;
|
|
||||||
duration: number;
|
duration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
onTimeFormatChange,
|
onTimeFormatChange,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
ethSignature,
|
|
||||||
duration,
|
duration,
|
||||||
}) => {
|
}) => {
|
||||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
@@ -119,7 +117,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
timeFormat,
|
timeFormat,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +161,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => reserveSlot(slot)}
|
onClick={() => reserveSlot(slot)}
|
||||||
data-testid="time">
|
data-testid="time"
|
||||||
|
data-disabled="false">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && (
|
{!!seatsPerTimeSlot && (
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
|
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">
|
||||||
|
{eventType.team?.parent?.name} {profile.name}
|
||||||
|
</h2>
|
||||||
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
data-testid="cancel_reason"
|
||||||
ref={cancelBookingRef}
|
ref={cancelBookingRef}
|
||||||
placeholder={t("cancellation_reason_placeholder")}
|
placeholder={t("cancellation_reason_placeholder")}
|
||||||
value={cancellationReason}
|
value={cancellationReason}
|
||||||
@@ -75,7 +76,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-testid="cancel"
|
data-testid="confirm_cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const SlotPicker = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
weekStart = 0,
|
weekStart = 0,
|
||||||
ethSignature,
|
|
||||||
}: {
|
}: {
|
||||||
eventType: Pick<
|
eventType: Pick<
|
||||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||||
@@ -94,7 +93,6 @@ export const SlotPicker = ({
|
|||||||
recurringEventCount?: number;
|
recurringEventCount?: number;
|
||||||
users: string[];
|
users: string[];
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
ethSignature?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
@@ -193,7 +191,6 @@ export const SlotPicker = ({
|
|||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
bookingAttendees={bookingAttendees}
|
bookingAttendees={bookingAttendees}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
ethSignature={ethSignature}
|
|
||||||
duration={parseInt(duration)}
|
duration={parseInt(duration)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -27,8 +26,6 @@ import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
|
||||||
import Gates from "@components/Gates";
|
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import { SlotPicker } from "@components/booking/SlotPicker";
|
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||||
|
|
||||||
@@ -89,14 +86,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setIs24hClockInLocalStorage(is24Hours);
|
setIs24hClockInLocalStorage(is24Hours);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [gateState, gateDispatcher] = useReducer(
|
|
||||||
(state: GateState, newState: Partial<GateState>) => ({
|
|
||||||
...state,
|
|
||||||
...newState,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
@@ -124,20 +113,11 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
|
|
||||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
|
|
||||||
// Define conditional gates here
|
|
||||||
const gates = [
|
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
|
||||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
|
||||||
? ("rainbow" as Gate)
|
|
||||||
: undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
||||||
{
|
{
|
||||||
seatReferenceUid: rescheduleUid,
|
seatReferenceUid: rescheduleUid,
|
||||||
@@ -148,7 +128,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||||
@@ -296,7 +276,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||||
bookingAttendees={bookingAttendees || undefined}
|
bookingAttendees={bookingAttendees || undefined}
|
||||||
recurringEventCount={recurringEventCount}
|
recurringEventCount={recurringEventCount}
|
||||||
ethSignature={gateState.rainbowToken}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,7 +285,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm, useFormContext } from "react-hook-form";
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -12,7 +12,6 @@ import { z } from "zod";
|
|||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
@@ -51,8 +50,6 @@ import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/
|
|||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
|
||||||
import Gates from "@components/Gates";
|
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
|
|
||||||
import type { BookPageProps } from "../../../pages/[user]/book";
|
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||||
@@ -224,13 +221,6 @@ const BookingPage = ({
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [gateState, gateDispatcher] = useReducer(
|
|
||||||
(state: GateState, newState: Partial<GateState>) => ({
|
|
||||||
...state,
|
|
||||||
...newState,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { timezone } = useTimePreferences();
|
const { timezone } = useTimePreferences();
|
||||||
|
|
||||||
@@ -488,7 +478,6 @@ const BookingPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
ethSignature: gateState.rainbowToken,
|
|
||||||
}));
|
}));
|
||||||
recurringMutation.mutate(recurringBookings);
|
recurringMutation.mutate(recurringBookings);
|
||||||
} else {
|
} else {
|
||||||
@@ -506,25 +495,15 @@ const BookingPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
ethSignature: gateState.rainbowToken,
|
|
||||||
seatReferenceUid: router.query.seatReferenceUid as string,
|
seatReferenceUid: router.query.seatReferenceUid as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
|
||||||
|
|
||||||
// Define conditional gates here
|
|
||||||
const gates = [
|
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
|
||||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
|
||||||
? ("rainbow" as Gate)
|
|
||||||
: undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{rescheduleUid
|
{rescheduleUid
|
||||||
@@ -686,7 +665,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</Gates>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,12 +57,15 @@ const LocationInput = (props: {
|
|||||||
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||||
);
|
);
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||||
|
const { defaultValue, ...rest } = remainingProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
control={control}
|
control={control}
|
||||||
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
return <PhoneInput onChange={onChange} value={value} {...rest} />;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -88,6 +91,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
locationFormMethods.setValue("locationType", selection?.value);
|
locationFormMethods.setValue("locationType", selection?.value);
|
||||||
|
if (selection?.address) {
|
||||||
|
locationFormMethods.setValue("locationAddress", selection?.address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selection]);
|
}, [selection]);
|
||||||
@@ -149,12 +155,25 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
name: "locationType",
|
name: "locationType",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedAddrValue = useWatch({
|
||||||
|
control: locationFormMethods.control,
|
||||||
|
name: "locationAddress",
|
||||||
|
});
|
||||||
|
|
||||||
const eventLocationType = getEventLocationType(selectedLocation);
|
const eventLocationType = getEventLocationType(selectedLocation);
|
||||||
|
|
||||||
const defaultLocation = defaultValues?.find(
|
const defaultLocation = defaultValues?.find(
|
||||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
(location: { type: EventLocationType["type"]; address?: string }) => {
|
||||||
|
if (location.type === LocationType.InPerson) {
|
||||||
|
return location.type === eventLocationType?.type && location.address === selectedAddrValue;
|
||||||
|
} else {
|
||||||
|
return location.type === eventLocationType?.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(defaultLocation);
|
||||||
|
|
||||||
const LocationOptions = (() => {
|
const LocationOptions = (() => {
|
||||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||||
if (!eventLocationType.variable) {
|
if (!eventLocationType.variable) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
</p>
|
</p>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="reschedule_reason"
|
data-testid="reschedule_reason"
|
||||||
name={t("reschedule_reason")}
|
name={t("reason_for_reschedule")}
|
||||||
value={rescheduleReason}
|
value={rescheduleReason}
|
||||||
onChange={(e) => setRescheduleReason(e.target.value)}
|
onChange={(e) => setRescheduleReason(e.target.value)}
|
||||||
className="mb-5 sm:mb-6"
|
className="mb-5 sm:mb-6"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
allowDisablingHostConfirmationEmails,
|
allowDisablingHostConfirmationEmails,
|
||||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
|
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -65,7 +66,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
@@ -159,6 +160,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||||
|
<div>
|
||||||
|
<BookerLayoutSelector fallbackToUserSettings />
|
||||||
|
</div>
|
||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
title={t("booking_questions_title")}
|
title={t("booking_questions_title")}
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!shouldLockDisableProps("apps").disabled && (
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
<div>
|
<div className="bg-muted rounded-md p-8">
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
|
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{notInstalledApps?.map((app) => (
|
{notInstalledApps?.map((app) => (
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const EventSetupTab = (
|
|||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||||
|
|
||||||
const locationOptions = props.locationOptions.filter((option) => {
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
return !team ? option.label !== "Conferencing" : true;
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
@@ -136,9 +136,17 @@ export const EventSetupTab = (
|
|||||||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
const openLocationModal = (type: EventLocationType["type"], address = "") => {
|
||||||
const option = getLocationFromType(type, locationOptions);
|
const option = getLocationFromType(type, locationOptions);
|
||||||
setSelectedLocation(option);
|
if (option && option.value === LocationType.InPerson) {
|
||||||
|
const inPersonOption = {
|
||||||
|
...option,
|
||||||
|
address,
|
||||||
|
};
|
||||||
|
setSelectedLocation(inPersonOption);
|
||||||
|
} else {
|
||||||
|
setSelectedLocation(option);
|
||||||
|
}
|
||||||
setShowLocationModal(true);
|
setShowLocationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,10 +306,14 @@ export const EventSetupTab = (
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
locationFormMethods.setValue("locationType", location.type);
|
locationFormMethods.setValue("locationType", location.type);
|
||||||
locationFormMethods.unregister("locationLink");
|
locationFormMethods.unregister("locationLink");
|
||||||
locationFormMethods.unregister("locationAddress");
|
if (location.type === LocationType.InPerson) {
|
||||||
|
locationFormMethods.setValue("locationAddress", location.address);
|
||||||
|
} else {
|
||||||
|
locationFormMethods.unregister("locationAddress");
|
||||||
|
}
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
locationFormMethods.unregister("locationPhoneNumber");
|
||||||
setEditingLocationType(location.type);
|
setEditingLocationType(location.type);
|
||||||
openLocationModal(location.type);
|
openLocationModal(location.type, location.address);
|
||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||||
@@ -517,7 +529,18 @@ export const EventSetupTab = (
|
|||||||
defaultValues={formMethods.getValues("locations")}
|
defaultValues={formMethods.getValues("locations")}
|
||||||
selection={
|
selection={
|
||||||
selectedLocation
|
selectedLocation
|
||||||
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
|
? selectedLocation.address
|
||||||
|
? {
|
||||||
|
value: selectedLocation.value,
|
||||||
|
label: t(selectedLocation.label),
|
||||||
|
icon: selectedLocation.icon,
|
||||||
|
address: selectedLocation.address,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
value: selectedLocation.value,
|
||||||
|
label: t(selectedLocation.label),
|
||||||
|
icon: selectedLocation.icon,
|
||||||
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
setSelectedLocation={setSelectedLocation}
|
setSelectedLocation={setSelectedLocation}
|
||||||
|
|||||||
@@ -75,8 +75,7 @@ function getNavigation(props: {
|
|||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
availability: AvailabilityOption | undefined;
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||||
props;
|
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -178,6 +177,7 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
availability,
|
availability,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.splice(1, 0, {
|
navigation.splice(1, 0, {
|
||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
@@ -185,7 +185,7 @@ function EventTypeSingleLayout({
|
|||||||
info:
|
info:
|
||||||
isManagedEventType || isChildrenManagedEventType
|
isManagedEventType || isChildrenManagedEventType
|
||||||
? eventType.schedule === null
|
? eventType.schedule === null
|
||||||
? "Member's default schedule"
|
? "members_default_schedule"
|
||||||
: isChildrenManagedEventType
|
: isChildrenManagedEventType
|
||||||
? `${
|
? `${
|
||||||
eventType.scheduleName
|
eventType.scheduleName
|
||||||
@@ -193,7 +193,7 @@ function EventTypeSingleLayout({
|
|||||||
: `default_schedule_name`
|
: `default_schedule_name`
|
||||||
}`
|
}`
|
||||||
: eventType.scheduleName ?? `default_schedule_name`
|
: eventType.scheduleName ?? `default_schedule_name`
|
||||||
: `default_schedule_name`,
|
: eventType.scheduleName ?? `default_schedule_name`,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
@@ -220,7 +220,18 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
}, [
|
||||||
|
t,
|
||||||
|
eventType,
|
||||||
|
enabledAppsNumber,
|
||||||
|
installedAppsNumber,
|
||||||
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
|
isManagedEventType,
|
||||||
|
isChildrenManagedEventType,
|
||||||
|
team,
|
||||||
|
formMethods,
|
||||||
|
]);
|
||||||
|
|
||||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
@@ -236,7 +247,7 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{!eventType.metadata.managedEventConfig && (
|
{!eventType.metadata?.managedEventConfig && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -379,7 +390,7 @@ function EventTypeSingleLayout({
|
|||||||
<Button
|
<Button
|
||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={formMethods.formState.isSubmitting || isUpdateMutationLoading}
|
loading={isUpdateMutationLoading}
|
||||||
data-testid="update-eventtype"
|
data-testid="update-eventtype"
|
||||||
form="event-type-form">
|
form="event-type-form">
|
||||||
{t("save")}
|
{t("save")}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default function RequiresConfirmationController({
|
|||||||
if (!requiresConfirmation) {
|
if (!requiresConfirmation) {
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [requiresConfirmation]);
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface IConnectCalendarsProps {
|
|||||||
|
|
||||||
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||||
const { nextStep } = props;
|
const { nextStep } = props;
|
||||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery();
|
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||||
|
|
||||||
|
|||||||
@@ -8,23 +8,17 @@ import { md } from "@calcom/lib/markdownIt";
|
|||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { Avatar } from "@calcom/ui";
|
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
interface IUserProfileProps {
|
|
||||||
user: IOnboardingPageProps["user"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserProfile = (props: IUserProfileProps) => {
|
const UserProfile = () => {
|
||||||
const { user } = props;
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null);
|
||||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||||
defaultValues: { bio: user?.bio || "" },
|
defaultValues: { bio: user?.bio || "" },
|
||||||
});
|
});
|
||||||
@@ -76,7 +70,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
|
|
||||||
async function updateProfileHandler(event: FormEvent<HTMLFormElement>) {
|
async function updateProfileHandler(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const enteredAvatar = avatarRef.current.value;
|
const enteredAvatar = avatarRef.current?.value;
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
avatar: enteredAvatar,
|
avatar: enteredAvatar,
|
||||||
});
|
});
|
||||||
@@ -127,14 +121,16 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
buttonMsg={t("add_profile_photo")}
|
buttonMsg={t("add_profile_photo")}
|
||||||
handleAvatarChange={(newAvatar) => {
|
handleAvatarChange={(newAvatar) => {
|
||||||
avatarRef.current.value = newAvatar;
|
if (avatarRef.current) {
|
||||||
|
avatarRef.current.value = newAvatar;
|
||||||
|
}
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
window.HTMLInputElement.prototype,
|
window.HTMLInputElement.prototype,
|
||||||
"value"
|
"value"
|
||||||
)?.set;
|
)?.set;
|
||||||
nativeInputValueSetter?.call(avatarRef.current, newAvatar);
|
nativeInputValueSetter?.call(avatarRef.current, newAvatar);
|
||||||
const ev2 = new Event("input", { bubbles: true });
|
const ev2 = new Event("input", { bubbles: true });
|
||||||
avatarRef.current.dispatchEvent(ev2);
|
avatarRef.current?.dispatchEvent(ev2);
|
||||||
updateProfileHandler(ev2 as unknown as FormEvent<HTMLFormElement>);
|
updateProfileHandler(ev2 as unknown as FormEvent<HTMLFormElement>);
|
||||||
setImageSrc(newAvatar);
|
setImageSrc(newAvatar);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,15 +13,13 @@ import { ArrowRight } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
|
||||||
|
|
||||||
interface IUserSettingsProps {
|
interface IUserSettingsProps {
|
||||||
user: IOnboardingPageProps["user"];
|
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSettings = (props: IUserSettingsProps) => {
|
const UserSettings = (props: IUserSettingsProps) => {
|
||||||
const { user, nextStep } = props;
|
const { nextStep } = props;
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -69,7 +67,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Username textfield */}
|
{/* Username textfield */}
|
||||||
<UsernameAvailabilityField user={user} />
|
<UsernameAvailabilityField />
|
||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
@@ -36,7 +37,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(markdownToSafeHTML(member.bio)) }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/ut
|
|||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { User } from "@calcom/prisma/client";
|
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
|
||||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
@@ -29,7 +27,6 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<User, "username" | "metadata">;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +54,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
readonly: disabled,
|
readonly: disabled,
|
||||||
user,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||||
const [markAsError, setMarkAsError] = useState(false);
|
const [markAsError, setMarkAsError] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface ICustomUsernameProps {
|
|||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UsernameTextfield = (props: ICustomUsernameProps) => {
|
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const {
|
const {
|
||||||
currentUsername,
|
currentUsername,
|
||||||
@@ -31,6 +31,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
usernameRef,
|
usernameRef,
|
||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||||
const [markAsError, setMarkAsError] = useState(false);
|
const [markAsError, setMarkAsError] = useState(false);
|
||||||
@@ -79,7 +80,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
<div className="ms-2 me-2 flex flex-row space-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
@@ -116,9 +117,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
addOnLeading={
|
|
||||||
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
|
||||||
}
|
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
@@ -133,6 +131,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
setInputUsernameValue(event.target.value);
|
setInputUsernameValue(event.target.value);
|
||||||
}}
|
}}
|
||||||
data-testid="username-input"
|
data-testid="username-input"
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||||
@@ -142,7 +141,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 hidden md:inline">
|
<div className="mt-7 hidden md:inline">
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { useRouter } from "next/router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import type { User } from "@calcom/prisma/client";
|
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
@@ -17,14 +19,24 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
|
|||||||
interface UsernameAvailabilityFieldProps {
|
interface UsernameAvailabilityFieldProps {
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<User, "username" | "metadata">;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
|
||||||
|
return organization
|
||||||
|
? organization.slug
|
||||||
|
? `${organization.slug}.${subdomainSuffix()}`
|
||||||
|
: organization.metadata && organization.metadata.requestedSlug
|
||||||
|
? `${organization.metadata.requestedSlug}.${subdomainSuffix()}`
|
||||||
|
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")
|
||||||
|
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||||
|
}
|
||||||
|
|
||||||
export const UsernameAvailabilityField = ({
|
export const UsernameAvailabilityField = ({
|
||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
user,
|
|
||||||
}: UsernameAvailabilityFieldProps) => {
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||||
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||||
const { username: currentUsername, setQuery: setCurrentUsername } =
|
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||||
@@ -37,6 +49,8 @@ export const UsernameAvailabilityField = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const usernamePrefix = useUserNamePrefix(user.organization);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -51,7 +65,7 @@ export const UsernameAvailabilityField = ({
|
|||||||
setInputUsernameValue={onChange}
|
setInputUsernameValue={onChange}
|
||||||
onSuccessMutation={onSuccessMutation}
|
onSuccessMutation={onSuccessMutation}
|
||||||
onErrorMutation={onErrorMutation}
|
onErrorMutation={onErrorMutation}
|
||||||
user={user}
|
addOnLeading={usernamePrefix}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ export const CheckedSelect = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
styles={{
|
|
||||||
option: (styles, { isDisabled }) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
name={props.name}
|
name={props.name}
|
||||||
placeholder={props.placeholder || t("select")}
|
placeholder={props.placeholder || t("select")}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type LocationOption = {
|
|||||||
value: EventLocationType["type"];
|
value: EventLocationType["type"];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
address?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
|||||||
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
||||||
return opts.empty(query);
|
return opts.empty(query);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return opts.success(query as any);
|
return opts.success(query as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +89,10 @@ const withQuery = <
|
|||||||
TInput = inferProcedureInput<TQuery>,
|
TInput = inferProcedureInput<TQuery>,
|
||||||
TOutput = inferProcedureOutput<TQuery>
|
TOutput = inferProcedureOutput<TQuery>
|
||||||
>(
|
>(
|
||||||
queryProcedure: DecorateProcedure<TQuery, any, any>,
|
queryProcedure: DecorateProcedure<TQuery, inferProcedureInput<TQuery>, inferProcedureOutput<TQuery>>,
|
||||||
|
|
||||||
input?: TInput,
|
input?: TInput,
|
||||||
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
|
params?: UseTRPCQueryOptions<TQuery, TInput, TOutput, TOutput, TError>
|
||||||
) => {
|
) => {
|
||||||
return function WithQuery(
|
return function WithQuery(
|
||||||
opts: Omit<
|
opts: Omit<
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { NextRouter } from "next/router";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||||
|
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||||
@@ -205,6 +207,11 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const orgBrand = useOrgBrandingValues();
|
||||||
|
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
const AppProviders = (props: AppPropsWithChildren) => {
|
const AppProviders = (props: AppPropsWithChildren) => {
|
||||||
const session = trpc.viewer.public.session.useQuery().data;
|
const session = trpc.viewer.public.session.useQuery().data;
|
||||||
// No need to have intercom on public pages - Good for Page Performance
|
// No need to have intercom on public pages - Good for Page Performance
|
||||||
@@ -222,7 +229,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
isThemeSupported={props.Component.isThemeSupported}
|
isThemeSupported={props.Component.isThemeSupported}
|
||||||
isBookingPage={props.Component.isBookingPage}>
|
isBookingPage={props.Component.isBookingPage}>
|
||||||
<FeatureFlagsProvider>
|
<FeatureFlagsProvider>
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<OrgBrandProvider>
|
||||||
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
|
</OrgBrandProvider>
|
||||||
</FeatureFlagsProvider>
|
</FeatureFlagsProvider>
|
||||||
</CalcomThemeProvider>
|
</CalcomThemeProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -5,7 +5,25 @@ export default function useRouterQuery<T extends string>(name: T) {
|
|||||||
const existingQueryParams = router.asPath.split("?")[1];
|
const existingQueryParams = router.asPath.split("?")[1];
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(existingQueryParams);
|
const urlParams = new URLSearchParams(existingQueryParams);
|
||||||
const query = Object.fromEntries(urlParams);
|
const query: Record<string, string | string[]> = {};
|
||||||
|
// Following error is thrown by Typescript:
|
||||||
|
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
|
||||||
|
// We should change the target to higher ES2019 atleast maybe
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
for (const [key, value] of urlParams) {
|
||||||
|
if (!query[key]) {
|
||||||
|
query[key] = value;
|
||||||
|
} else {
|
||||||
|
let queryValue = query[key];
|
||||||
|
if (queryValue instanceof Array) {
|
||||||
|
queryValue.push(value);
|
||||||
|
} else {
|
||||||
|
queryValue = query[key] = [queryValue];
|
||||||
|
queryValue.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setQuery = (newValue: string | number | null | undefined) => {
|
const setQuery = (newValue: string | number | null | undefined) => {
|
||||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
type GetSSRResult<TProps> =
|
type GetSSRResult<TProps> =
|
||||||
//
|
//
|
||||||
{ props: TProps } | { redirect: any } | { notFound: boolean };
|
{ props: TProps } | { redirect: { destination: string; permanent: boolean } } | { notFound: boolean };
|
||||||
|
|
||||||
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;
|
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
|||||||
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||||
const ssrResponse = await getServerSideProps(context);
|
const ssrResponse = await getServerSideProps(context);
|
||||||
const embed = context.query.embed;
|
const embed = context.query.embed;
|
||||||
|
const layout = context.query.layout;
|
||||||
|
|
||||||
if ("redirect" in ssrResponse) {
|
if ("redirect" in ssrResponse) {
|
||||||
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
||||||
@@ -18,6 +19,8 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
|||||||
destinationUrlObj.pathname +
|
destinationUrlObj.pathname +
|
||||||
"/embed?" +
|
"/embed?" +
|
||||||
destinationUrlObj.searchParams.toString() +
|
destinationUrlObj.searchParams.toString() +
|
||||||
|
"&layout=" +
|
||||||
|
layout +
|
||||||
"&embed=" +
|
"&embed=" +
|
||||||
embed;
|
embed;
|
||||||
|
|
||||||
|
|||||||
+41
-7
@@ -3,6 +3,7 @@ import { collectEvents } from "next-collect/server";
|
|||||||
import type { NextMiddleware } from "next/server";
|
import type { NextMiddleware } from "next/server";
|
||||||
import { NextResponse, userAgent } from "next/server";
|
import { NextResponse, userAgent } from "next/server";
|
||||||
|
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||||
@@ -10,6 +11,25 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
|||||||
const middleware: NextMiddleware = async (req) => {
|
const middleware: NextMiddleware = async (req) => {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
const requestHeaders = new Headers(req.headers);
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.get("host") ?? "");
|
||||||
|
/**
|
||||||
|
* We are using env variable to toggle new-booker because using flags would be an unnecessary delay for booking pages
|
||||||
|
* Also, we can't easily identify the booker page requests here(to just fetch the flags for those requests)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Make sure we are in the presence of an organization
|
||||||
|
if (isValidOrgDomain && url.pathname === "/") {
|
||||||
|
// In the presence of an organization, cover its profile page at "/"
|
||||||
|
// rewrites for org profile page using team profile page
|
||||||
|
url.pathname = `/org/${currentOrgDomain}`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
|
||||||
|
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
|
||||||
|
req.nextUrl.pathname = "/api/nope";
|
||||||
|
return NextResponse.redirect(req.nextUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (!url.pathname.startsWith("/api")) {
|
if (!url.pathname.startsWith("/api")) {
|
||||||
//
|
//
|
||||||
@@ -48,13 +68,6 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that embed query param is there in when /embed is added.
|
|
||||||
// query param is the way in which client side code knows that it is in embed mode.
|
|
||||||
if (url.pathname.endsWith("/embed") && typeof url.searchParams.get("embed") !== "string") {
|
|
||||||
url.searchParams.set("embed", "");
|
|
||||||
return NextResponse.redirect(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't 404 old routing_forms links
|
// Don't 404 old routing_forms links
|
||||||
if (url.pathname.startsWith("/apps/routing_forms")) {
|
if (url.pathname.startsWith("/apps/routing_forms")) {
|
||||||
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
||||||
@@ -70,6 +83,26 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
requestHeaders.set("x-csp-enforce", "true");
|
requestHeaders.set("x-csp-enforce", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isValidOrgDomain) {
|
||||||
|
// Match /:slug to determine if it corresponds to org subteam slug or org user slug
|
||||||
|
const slugs = /^\/([^/]+)(\/[^/]+)?$/.exec(url.pathname);
|
||||||
|
// In the presence of an organization, if not team profile, a user or team is being accessed
|
||||||
|
if (slugs) {
|
||||||
|
const [_, teamName, eventType] = slugs;
|
||||||
|
// Fetch the corresponding subteams for the entered organization
|
||||||
|
const getSubteams = await fetch(`${WEBAPP_URL}/api/organizations/${currentOrgDomain}/subteams`);
|
||||||
|
if (getSubteams.ok) {
|
||||||
|
const data = await getSubteams.json();
|
||||||
|
// Treat entered slug as a team if found in the subteams fetched
|
||||||
|
if (data.slugs.includes(teamName)) {
|
||||||
|
// Rewriting towards /team/:slug to bring up the team profile within the org
|
||||||
|
url.pathname = `/team/${teamName}${eventType ?? ""}`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next({
|
return NextResponse.next({
|
||||||
request: {
|
request: {
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
@@ -79,6 +112,7 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
"/:path*",
|
||||||
"/api/collect-events/:path*",
|
"/api/collect-events/:path*",
|
||||||
"/api/auth/:path*",
|
"/api/auth/:path*",
|
||||||
"/apps/routing_forms/:path*",
|
"/apps/routing_forms/:path*",
|
||||||
|
|||||||
+104
-12
@@ -2,7 +2,7 @@ require("dotenv").config({ path: "../../.env" });
|
|||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
|
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
|
|
||||||
@@ -57,6 +57,20 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const informAboutDuplicateTranslations = () => {
|
||||||
|
const valueSet = new Set();
|
||||||
|
|
||||||
|
for (const key in englishTranslation) {
|
||||||
|
if (valueSet.has(englishTranslation[key])) {
|
||||||
|
console.warn("\x1b[33mDuplicate value found in:", "\x1b[0m", key);
|
||||||
|
} else {
|
||||||
|
valueSet.add(englishTranslation[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
informAboutDuplicateTranslations();
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
if (process.env.ANALYZE === "true") {
|
if (process.env.ANALYZE === "true") {
|
||||||
// only load dependency if env `ANALYZE` was set
|
// only load dependency if env `ANALYZE` was set
|
||||||
@@ -169,7 +183,16 @@ const nextConfig = {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
||||||
|
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
||||||
|
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
||||||
|
const userTypeRouteRegExp = `/:user((?!${pages.join("|")})[^/]*)/:type`;
|
||||||
|
|
||||||
|
let rewrites = [
|
||||||
|
{
|
||||||
|
source: "/org/:slug",
|
||||||
|
destination: "/team/:slug",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: "/:user/avatar.png",
|
source: "/:user/avatar.png",
|
||||||
destination: "/api/user/avatar?username=:user",
|
destination: "/api/user/avatar?username=:user",
|
||||||
@@ -201,21 +224,85 @@ const nextConfig = {
|
|||||||
source: "/cancel/:path*",
|
source: "/cancel/:path*",
|
||||||
destination: "/booking/:path*",
|
destination: "/booking/:path*",
|
||||||
},
|
},
|
||||||
|
// Keep cookie based booker enabled just in case we disable new-booker globally
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: userTypeRouteRegExp,
|
||||||
|
destination: "/new-booker/:user/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type",
|
||||||
|
destination: "/new-booker/team/:slug/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/d/:link/:slug",
|
||||||
|
destination: "/new-booker/d/:link/:slug",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Keep cookie based booker enabled to test new-booker embed in production
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||||
|
destination: "/new-booker/:user/:type/embed",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type/embed",
|
||||||
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
/* TODO: have these files being served from another deployment or CDN {
|
/* TODO: have these files being served from another deployment or CDN {
|
||||||
source: "/embed/embed.js",
|
source: "/embed/embed.js",
|
||||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||||
}, */
|
}, */
|
||||||
{
|
|
||||||
source: `/:user((?!${pages.join("|")}).*)/:type`,
|
/**
|
||||||
destination: "/new-booker/:user/:type",
|
* Enables new booker using cookie. It works even if NEW_BOOKER_ENABLED_FOR_NON_EMBED, NEW_BOOKER_ENABLED_FOR_EMBED are disabled
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
*/
|
||||||
},
|
|
||||||
{
|
|
||||||
source: "/team/:slug/:type",
|
|
||||||
destination: "/new-booker/team/:slug/:type",
|
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Enable New Booker for all Embed Requests
|
||||||
|
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
|
||||||
|
console.log("Enabling New Booker for Embed");
|
||||||
|
rewrites.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||||
|
destination: "/new-booker/:user/:type/embed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type/embed",
|
||||||
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable New Booker for All but embed Requests
|
||||||
|
if (process.env.NEW_BOOKER_ENABLED_FOR_NON_EMBED === "1") {
|
||||||
|
console.log("Enabling New Booker for Non-Embed");
|
||||||
|
rewrites.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
source: userTypeRouteRegExp,
|
||||||
|
destination: "/new-booker/:user/:type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type",
|
||||||
|
destination: "/new-booker/team/:slug/:type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/d/:link/:slug",
|
||||||
|
destination: "/new-booker/d/:link/:slug",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rewrites;
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
@@ -274,6 +361,11 @@ const nextConfig = {
|
|||||||
destination: "/teams",
|
destination: "/teams",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/settings/admin",
|
||||||
|
destination: "/settings/admin/flags",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
/* V2 testers get redirected to the new settings */
|
/* V2 testers get redirected to the new settings */
|
||||||
{
|
{
|
||||||
source: "/settings/profile",
|
source: "/settings/profile",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.9.5",
|
"version": "3.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
"@tremor/react": "^2.0.0",
|
"@tremor/react": "^2.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
|
"@upstash/ratelimit": "^0.4.3",
|
||||||
|
"@upstash/redis": "^1.21.0",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.5.0",
|
"@vercel/og": "^0.5.0",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
@@ -255,6 +256,7 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const crypto = await import("crypto");
|
const crypto = await import("crypto");
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
|
||||||
const usernameList = getUsernameList(context.query.user as string);
|
const usernameList = getUsernameList(context.query.user as string);
|
||||||
const dataFetchStart = Date.now();
|
const dataFetchStart = Date.now();
|
||||||
@@ -263,6 +265,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
username: {
|
username: {
|
||||||
in: usernameList,
|
in: usernameList,
|
||||||
},
|
},
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -272,6 +279,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
bio: true,
|
bio: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
|
organizationId: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
@@ -284,7 +292,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!users.length) {
|
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
} as {
|
} as {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
@@ -15,7 +17,9 @@ import type { EmbedProps } from "@lib/withEmbedSsr";
|
|||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||||
|
|
||||||
export default function Type(props: AvailabilityPageProps) {
|
export default function Type(props: AvailabilityPageProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -50,6 +54,21 @@ export default function Type(props: AvailabilityPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
) : !props.isValidOrgDomain && props.organizationContext ? (
|
||||||
|
<div className="dark:bg-darkgray-50 h-screen">
|
||||||
|
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||||
|
<div className="space-y-6" data-testid="event-types">
|
||||||
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
|
<div className="text-muted dark:text-inverted p-8 text-center">
|
||||||
|
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
|
||||||
|
{" " + t("unavailable")}
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-md">{t("user_belongs_organization")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AvailabilityPage {...props} />
|
<AvailabilityPage {...props} />
|
||||||
);
|
);
|
||||||
@@ -59,21 +78,25 @@ Type.isBookingPage = true;
|
|||||||
Type.PageWrapper = PageWrapper;
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||||
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
|
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
const { type: slug, user: username } = paramsSchema.parse(context.query);
|
||||||
|
|
||||||
const { type: slug, user: username } = paramsSchema.parse(context.params);
|
const user = await prisma.user.findFirst({
|
||||||
const ssg = await ssgInit(context);
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
where: {
|
||||||
/** TODO: We should standarize this */
|
/** TODO: We should standarize this */
|
||||||
username: username.toLowerCase().replace(/( |%20)/g, "+"),
|
username: username.toLowerCase().replace(/( |%20)/g, "+"),
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -87,6 +110,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
organizationId: true,
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
where: {
|
where: {
|
||||||
// Many-to-many relationship causes inclusion of the team events - cool -
|
// Many-to-many relationship causes inclusion of the team events - cool -
|
||||||
@@ -108,6 +132,17 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
logo: true,
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
logo: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
@@ -179,16 +214,17 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
// Dynamic group has no theme preference right now. It uses system theme.
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
themeBasis: user.username,
|
themeBasis: user.username,
|
||||||
|
organizationContext: user?.organizationId !== null,
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
|
||||||
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
|
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
|
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
@@ -197,10 +233,10 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
|
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
|
||||||
"@calcom/prisma/zod-utils"
|
"@calcom/prisma/zod-utils"
|
||||||
);
|
);
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
const { getAppFromSlug } = await import("@calcom/app-store/utils");
|
const { getAppFromSlug } = await import("@calcom/app-store/utils");
|
||||||
|
|
||||||
const ssg = await ssgInit(context);
|
|
||||||
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
||||||
const usernameList = getUsernameList(userParam);
|
const usernameList = getUsernameList(userParam);
|
||||||
const length = parseInt(typeParam);
|
const length = parseInt(typeParam);
|
||||||
@@ -230,6 +266,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
organizationId: true,
|
||||||
away: true,
|
away: true,
|
||||||
schedules: {
|
schedules: {
|
||||||
select: {
|
select: {
|
||||||
@@ -313,24 +350,21 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
themeBasis: null,
|
themeBasis: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
organizationContext: !users.some((user) => user.organizationId === null),
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
|
||||||
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { user: userParam } = paramsSchema.parse(context.params);
|
const { user: userParam } = paramsSchema.parse(context.params);
|
||||||
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
||||||
const isDynamicGroup = userParam.includes("+");
|
const isDynamicGroup = getUsernameList(userParam).length > 1;
|
||||||
if (isDynamicGroup) {
|
if (isDynamicGroup) {
|
||||||
return await getDynamicGroupPageProps(context);
|
return await getDynamicGroupPageProps(context);
|
||||||
} else {
|
} else {
|
||||||
return await getUserPageProps(context);
|
return await getUserPageProps(context);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
return { paths: [], fallback: "blocking" };
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import type { GetStaticPropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { getStaticProps as _getStaticProps } from "../[type]";
|
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||||
|
|
||||||
export { getStaticPaths } from "../[type]";
|
|
||||||
|
|
||||||
export { default } from "../[type]";
|
export { default } from "../[type]";
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const staticResponse = await _getStaticProps(context);
|
const ssrResponse = await _getServerSideProps(context);
|
||||||
if (staticResponse.notFound) {
|
if (ssrResponse.notFound) {
|
||||||
return staticResponse;
|
return ssrResponse;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...staticResponse,
|
...ssrResponse,
|
||||||
props: {
|
props: {
|
||||||
...staticResponse.props,
|
...ssrResponse.props,
|
||||||
isEmbed: true,
|
isEmbed: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { getStaticProps, getStaticPaths, default } from "./index";
|
||||||
+14
-13
@@ -3,22 +3,20 @@
|
|||||||
* caching system that NextJS uses SSG pages.
|
* caching system that NextJS uses SSG pages.
|
||||||
* TODO: Redirect to user profile on browser
|
* TODO: Redirect to user profile on browser
|
||||||
*/
|
*/
|
||||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getCachedResults } from "@calcom/core";
|
import getCalendarsEvents from "@calcom/core/getCalendarsEvents";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const CalendarCache = () => <div />;
|
|
||||||
|
|
||||||
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
||||||
export const getStaticProps: GetStaticProps<
|
export const getStaticProps: GetStaticProps<
|
||||||
{ results: Awaited<ReturnType<typeof getCachedResults>> },
|
{ results: Awaited<ReturnType<typeof getCalendarsEvents>> },
|
||||||
{ user: string }
|
{ user: string }
|
||||||
> = async (context) => {
|
> = async (context) => {
|
||||||
const { user: username, month } = paramsSchema.parse(context.params);
|
const { user: username, month } = paramsSchema.parse(context.params);
|
||||||
const userWithCredentials = await prisma.user.findUnique({
|
const userWithCredentials = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
},
|
},
|
||||||
@@ -29,16 +27,16 @@ export const getStaticProps: GetStaticProps<
|
|||||||
selectedCalendars: true,
|
selectedCalendars: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const startDate = (
|
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||||
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs.utc() : dayjs.utc(month, "YYYY-MM")
|
const startDate = dayjs.utc(month, "YYYY-MM").startOf("day").subtract(11, "hours").format();
|
||||||
).startOf("day");
|
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||||
const endDate = startDate.endOf("month");
|
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
|
||||||
try {
|
try {
|
||||||
const results = userWithCredentials?.credentials
|
const results = userWithCredentials?.credentials
|
||||||
? await getCachedResults(
|
? await getCalendarsEvents(
|
||||||
userWithCredentials?.credentials,
|
userWithCredentials?.credentials,
|
||||||
startDate.format(),
|
startDate,
|
||||||
endDate.format(),
|
endDate,
|
||||||
userWithCredentials?.selectedCalendars
|
userWithCredentials?.selectedCalendars
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
@@ -64,5 +62,8 @@ export const getStaticPaths: GetStaticPaths = () => {
|
|||||||
fallback: "blocking",
|
fallback: "blocking",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticProps>;
|
||||||
|
const CalendarCache = (props: Props) =>
|
||||||
|
process.env.NODE_ENV === "development" ? <pre>{JSON.stringify(props, null, " ")}</pre> : <div />;
|
||||||
|
|
||||||
export default CalendarCache;
|
export default CalendarCache;
|
||||||
@@ -5,15 +5,12 @@ import { z } from "zod";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendPasswordResetEmail } from "@calcom/emails";
|
import { sendPasswordResetEmail } from "@calcom/emails";
|
||||||
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
||||||
import rateLimit from "@calcom/lib/rateLimit";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
|
import rateLimiter from "@calcom/lib/rateLimit";
|
||||||
import { defaultHandler } from "@calcom/lib/server";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
const limiter = rateLimit({
|
|
||||||
intervalInMs: 60 * 1000, // 1 minute
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||||
|
|
||||||
@@ -37,10 +34,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
// 10 requests per minute
|
// 10 requests per minute
|
||||||
|
|
||||||
try {
|
const limiter = await rateLimiter();
|
||||||
limiter.check(10, ip);
|
const limit = await limiter({
|
||||||
} catch (e) {
|
identifier: ip,
|
||||||
return res.status(429).json({ message: "Too Many Requests." });
|
});
|
||||||
|
|
||||||
|
if (!limit.success) {
|
||||||
|
throw new Error(ErrorCode.RateLimitExceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||||
|
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
const signupSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(7),
|
||||||
|
language: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return;
|
return;
|
||||||
@@ -17,7 +26,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
const { email, password } = data;
|
const { email, password, language } = signupSchema.parse(data);
|
||||||
|
|
||||||
const username = slugify(data.username);
|
const username = slugify(data.username);
|
||||||
const userEmail = email.toLowerCase();
|
const userEmail = email.toLowerCase();
|
||||||
|
|
||||||
@@ -26,16 +36,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEmail || !userEmail.includes("@")) {
|
|
||||||
res.status(422).json({ message: "Invalid email" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password || password.trim().length < 7) {
|
|
||||||
res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is an existingUser if the username matches
|
// There is an existingUser if the username matches
|
||||||
// OR if the email matches AND either the email is verified
|
// OR if the email matches AND either the email is verified
|
||||||
// or both username and password are set
|
// or both username and password are set
|
||||||
@@ -106,5 +106,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await sendEmailVerification({
|
||||||
|
email: userEmail,
|
||||||
|
username,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({ message: "Created user" });
|
res.status(201).json({ message: "Created user" });
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
const verifySchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { token } = verifySchema.parse(req.query);
|
||||||
|
|
||||||
|
const foundToken = await prisma.verificationToken.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundToken) {
|
||||||
|
return res.status(401).json({ message: "No token found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayjs(foundToken?.expires).isBefore(dayjs())) {
|
||||||
|
return res.status(401).json({ message: "Token expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: foundToken?.identifier,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete token from DB after it has been used
|
||||||
|
await prisma.verificationToken.delete({
|
||||||
|
where: {
|
||||||
|
id: foundToken?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCompletedOnboarding = user.completedOnboarding;
|
||||||
|
|
||||||
|
res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
|
||||||
|
}
|
||||||
@@ -60,9 +60,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("OrganizerRequestEmail", {
|
renderEmail("VerifyAccountEmail", {
|
||||||
calEvent: evt,
|
language: t,
|
||||||
attendee: evt.organizer,
|
user: {
|
||||||
|
name: "Pro Example",
|
||||||
|
email: "pro@example.com",
|
||||||
|
},
|
||||||
|
verificationEmailLink:
|
||||||
|
"http://localhost:3000/api/auth/verify-email?token=b91af0eee5a9a24a8d83a3d3d6a58c1606496e94ced589441649273c66100f5b",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
||||||
@@ -26,7 +27,7 @@ const decryptedSchema = z.object({
|
|||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
const { action, token, reason } = querySchema.parse(req.query);
|
const { action, token, reason } = querySchema.parse(req.query);
|
||||||
const { bookingUid } = decryptedSchema.parse(
|
const { bookingUid, userId } = decryptedSchema.parse(
|
||||||
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -34,10 +35,32 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||||||
where: { uid: bookingUid },
|
where: { uid: bookingUid },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
/** We shape the session as required by tRPC router */
|
||||||
|
async function sessionGetter() {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username: "" /* Not used in this context */,
|
||||||
|
role: UserPermissionRole.USER,
|
||||||
|
},
|
||||||
|
hasValidLicense: true,
|
||||||
|
expires: "" /* Not used in this context */,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @see https://trpc.io/docs/server-side-calls */
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
const ctx = await createContext({ req, res });
|
const ctx = await createContext({ req, res }, sessionGetter);
|
||||||
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
const caller = viewerRouter.createCaller({
|
||||||
|
...ctx,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
user: { ...user, locale: user?.locale ?? "en" },
|
||||||
|
});
|
||||||
await caller.bookings.confirm({
|
await caller.bookings.confirm({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
recurringEventId: booking.recurringEventId || undefined,
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
@@ -46,7 +69,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let message = "Error confirming booking";
|
let message = "Error confirming booking";
|
||||||
if (e instanceof TRPCError) message = e.message;
|
if (e instanceof TRPCError) message = (e as TRPCError).message;
|
||||||
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import {
|
import {
|
||||||
ANDROID_CHROME_ICON_192,
|
ANDROID_CHROME_ICON_192,
|
||||||
ANDROID_CHROME_ICON_256,
|
ANDROID_CHROME_ICON_256,
|
||||||
@@ -104,7 +105,7 @@ function isValidLogoType(type: string): type is LogoType {
|
|||||||
return type in logoDefinitions;
|
return type in logoDefinitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTeamLogos(subdomain: string) {
|
async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
// if not cal.com
|
// if not cal.com
|
||||||
@@ -118,9 +119,15 @@ async function getTeamLogos(subdomain: string) {
|
|||||||
}
|
}
|
||||||
// load from DB
|
// load from DB
|
||||||
const { default: prisma } = await import("@calcom/prisma");
|
const { default: prisma } = await import("@calcom/prisma");
|
||||||
const team = await prisma.team.findUnique({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
slug: subdomain,
|
slug: subdomain,
|
||||||
|
...(isValidOrgDomain && {
|
||||||
|
metadata: {
|
||||||
|
path: ["isOrganization"],
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
appLogo: true,
|
appLogo: true,
|
||||||
@@ -147,6 +154,7 @@ async function getTeamLogos(subdomain: string) {
|
|||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const parsedQuery = logoApiSchema.parse(query);
|
const parsedQuery = logoApiSchema.parse(query);
|
||||||
|
const { isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
|
|
||||||
const hostname = req?.headers["host"];
|
const hostname = req?.headers["host"];
|
||||||
if (!hostname) throw new Error("No hostname");
|
if (!hostname) throw new Error("No hostname");
|
||||||
@@ -154,7 +162,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
if (!domains) throw new Error("No domains");
|
if (!domains) throw new Error("No domains");
|
||||||
|
|
||||||
const [subdomain] = domains;
|
const [subdomain] = domains;
|
||||||
const teamLogos = await getTeamLogos(subdomain);
|
const teamLogos = await getTeamLogos(subdomain, isValidOrgDomain);
|
||||||
|
|
||||||
// Resolve all icon types to team logos, falling back to Cal.com defaults.
|
// Resolve all icon types to team logos, falling back to Cal.com defaults.
|
||||||
const type: LogoType = parsedQuery?.type && isValidLogoType(parsedQuery.type) ? parsedQuery.type : "logo";
|
const type: LogoType = parsedQuery?.type && isValidLogoType(parsedQuery.type) ? parsedQuery.type : "logo";
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "@calcom/features/ee/organizations/api/subteams";
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
|
|
||||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
|
||||||
|
|
||||||
export default createNextApiHandler(ethRouter);
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||||
|
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||||
|
|
||||||
|
export default createNextApiHandler(viewerOrganizationsRouter);
|
||||||
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -16,10 +17,18 @@ const querySchema = z
|
|||||||
|
|
||||||
async function getIdentityData(req: NextApiRequest) {
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
const { username, teamname } = querySchema.parse(req.query);
|
const { username, teamname } = querySchema.parse(req.query);
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: { username },
|
where: {
|
||||||
|
username,
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
select: { avatar: true, email: true },
|
select: { avatar: true, email: true },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -29,8 +38,15 @@ async function getIdentityData(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (teamname) {
|
if (teamname) {
|
||||||
const team = await prisma.team.findUnique({
|
const team = await prisma.team.findFirst({
|
||||||
where: { slug: teamname },
|
where: {
|
||||||
|
slug: teamname,
|
||||||
|
parent: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
select: { logo: true },
|
select: { logo: true },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||||
|
|
||||||
type Response = {
|
type Response = {
|
||||||
@@ -8,6 +9,7 @@ type Response = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
||||||
const result = await checkUsername(req.body.username);
|
const { currentOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
|
const result = await checkUsername(req.body.username, currentOrgDomain);
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
|||||||
setBulkUpdateModal(true);
|
setBulkUpdateModal(true);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{t("change_default_conferencing_app")}
|
{t("set_as_default")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function Page() {
|
|||||||
callbackUrl: "/",
|
callbackUrl: "/",
|
||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|||||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
@@ -95,7 +96,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
|
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("enable_apps"),
|
title: t("enable_apps"),
|
||||||
description: t("enable_apps_description"),
|
description: t("enable_apps_description", { appName: APP_NAME }),
|
||||||
contentClassname: "!pb-0 mb-[-1px]",
|
contentClassname: "!pb-0 mb-[-1px]",
|
||||||
content: (setIsLoading) => {
|
content: (setIsLoading) => {
|
||||||
const currentStep = isFreeLicense ? 3 : 4;
|
const currentStep = isFreeLicense ? 3 : 4;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
|||||||
|
|
||||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||||
import {
|
import {
|
||||||
hostedCal,
|
hostedCal,
|
||||||
@@ -68,11 +69,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
const { currentOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
||||||
if (usernameParam && session.user.email) {
|
if (usernameParam && session.user.email) {
|
||||||
const availability = await checkUsername(usernameParam);
|
const availability = await checkUsername(usernameParam, currentOrgDomain);
|
||||||
if (availability.available && availability.premium) {
|
if (availability.available && availability.premium) {
|
||||||
const stripePremiumUrl = await getStripePremiumUsernameUrl({
|
const stripePremiumUrl = await getStripePremiumUsernameUrl({
|
||||||
userEmail: session.user.email,
|
userEmail: session.user.email,
|
||||||
@@ -111,8 +113,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const ret = await samlTenantProduct(prisma, emailParam);
|
const ret = await samlTenantProduct(prisma, emailParam);
|
||||||
tenant = ret.tenant;
|
tenant = ret.tenant;
|
||||||
product = ret.product;
|
product = ret.product;
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
error = e.message;
|
if (e instanceof Error) {
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function Page({ samlTenantID, samlProductID }: inferSSRProps<type
|
|||||||
if (HOSTED_CAL_FEATURES) {
|
if (HOSTED_CAL_FEATURES) {
|
||||||
router.push("/auth/login");
|
router.push("/auth/login");
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,6 +30,7 @@ export default function Page({ samlTenantID, samlProductID }: inferSSRProps<type
|
|||||||
},
|
},
|
||||||
{ tenant: samlTenantID, product: samlProductID }
|
{ tenant: samlTenantID, product: samlProductID }
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { MailOpenIcon } from "lucide-react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck";
|
||||||
|
import { Button, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
function VerifyEmailPage() {
|
||||||
|
const { data } = useEmailVerifyCheck();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useLocale();
|
||||||
|
const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.isVerified) {
|
||||||
|
router.replace("/getting-started");
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data?.isVerified]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[100vh] w-full ">
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<EmptyScreen
|
||||||
|
border
|
||||||
|
dashedBorder={false}
|
||||||
|
Icon={MailOpenIcon}
|
||||||
|
headline={t("check_your_email")}
|
||||||
|
description={t("verify_email_page_body", { email: session?.user?.email, appName: APP_NAME })}
|
||||||
|
className="bg-default"
|
||||||
|
buttonRaw={
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
className="underline"
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
onClick={() => {
|
||||||
|
showToast("Send email", "success");
|
||||||
|
mutation.mutate();
|
||||||
|
}}>
|
||||||
|
Resend Email
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VerifyEmailPage;
|
||||||
|
|
||||||
|
VerifyEmailPage.PageWrapper = PageWrapper;
|
||||||
@@ -105,7 +105,8 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const WithQuery = withQuery(trpc.viewer.availability.list);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const WithQuery = withQuery(trpc.viewer.availability.list as any);
|
||||||
|
|
||||||
export default function AvailabilityPage() {
|
export default function AvailabilityPage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||||
{
|
{
|
||||||
username: user.username!,
|
username: user.username || "",
|
||||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
dateFrom: selectedDate.startOf("day").utc().format(),
|
||||||
dateTo: selectedDate.endOf("day").utc().format(),
|
dateTo: selectedDate.endOf("day").utc().format(),
|
||||||
withSource: true,
|
withSource: true,
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export default function Success(props: SuccessProps) {
|
|||||||
allRemainingBookings,
|
allRemainingBookings,
|
||||||
isSuccessBookingPage,
|
isSuccessBookingPage,
|
||||||
cancel: isCancellationMode,
|
cancel: isCancellationMode,
|
||||||
changes,
|
|
||||||
formerTime,
|
formerTime,
|
||||||
email,
|
email,
|
||||||
seatReferenceUid,
|
seatReferenceUid,
|
||||||
@@ -222,6 +221,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
setCalculatedDuration(
|
setCalculatedDuration(
|
||||||
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
|
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function eventLink(): string {
|
function eventLink(): string {
|
||||||
@@ -279,9 +279,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
darkBrandColor: props.profile.darkBrandColor,
|
darkBrandColor: props.profile.darkBrandColor,
|
||||||
});
|
});
|
||||||
const title = t(
|
const title = t(
|
||||||
`booking_${needsConfirmation ? "booking_submitted" : "confirmed"}${
|
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
||||||
props.recurringBookings ? "_recurring" : ""
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const locationToDisplay = getSuccessPageLocationMessage(
|
const locationToDisplay = getSuccessPageLocationMessage(
|
||||||
@@ -365,7 +363,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
id="modal-headline">
|
id="modal-headline">
|
||||||
{needsConfirmation && !isCancelled
|
{needsConfirmation && !isCancelled
|
||||||
? props.recurringBookings
|
? props.recurringBookings
|
||||||
? t("submitted_recurring")
|
? t("booking_submitted_recurring")
|
||||||
: t("booking_submitted")
|
: t("booking_submitted")
|
||||||
: isCancelled
|
: isCancelled
|
||||||
? seatReferenceUid
|
? seatReferenceUid
|
||||||
@@ -402,7 +400,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="font-medium">{t("what")}</div>
|
<div className="font-medium">{t("what")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</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 && (
|
{reschedule && !!formerTime && (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import SkeletonLoader from "@components/booking/SkeletonLoader";
|
|||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
type BookingListingStatus = z.infer<typeof filterQuerySchema>["status"];
|
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
|
||||||
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
||||||
|
|
||||||
type RecurringInfo = {
|
type RecurringInfo = {
|
||||||
@@ -34,7 +34,7 @@ type RecurringInfo = {
|
|||||||
|
|
||||||
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<NonNullable<BookingListingStatus>, string> = {
|
||||||
upcoming: "upcoming_bookings",
|
upcoming: "upcoming_bookings",
|
||||||
recurring: "recurring_bookings",
|
recurring: "recurring_bookings",
|
||||||
past: "past_bookings",
|
past: "past_bookings",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -17,9 +17,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
|
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type {
|
||||||
|
BookerLayoutSettings,
|
||||||
|
customInputSchema,
|
||||||
|
EventTypeMetaDataSchema,
|
||||||
|
} from "@calcom/prisma/zod-utils";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -96,6 +101,7 @@ export type FormValues = {
|
|||||||
hosts: { userId: number; isFixed: boolean }[];
|
hosts: { userId: number; isFixed: boolean }[];
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||||
availability?: AvailabilityOption;
|
availability?: AvailabilityOption;
|
||||||
|
bookerLayouts: BookerLayoutSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -184,17 +190,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
|
|
||||||
const metadata = eventType.metadata;
|
const metadata = eventType.metadata;
|
||||||
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
|
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
|
||||||
if (!!team) {
|
if (!!team && metadata !== null) {
|
||||||
metadata.config = {
|
metadata.config = {
|
||||||
...metadata.config,
|
...metadata.config,
|
||||||
useHostSchedulesForTeamEvent:
|
useHostSchedulesForTeamEvent:
|
||||||
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
|
typeof eventType.metadata?.config?.useHostSchedulesForTeamEvent !== "undefined"
|
||||||
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
|
? eventType.metadata?.config?.useHostSchedulesForTeamEvent === true
|
||||||
: !!eventType.schedule,
|
: !!eventType.schedule,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Make sure non-team events NEVER have this config key;
|
// Make sure non-team events NEVER have this config key;
|
||||||
delete metadata.config?.useHostSchedulesForTeamEvent;
|
delete metadata?.config?.useHostSchedulesForTeamEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingFields: Prisma.JsonObject = {};
|
const bookingFields: Prisma.JsonObject = {};
|
||||||
@@ -203,41 +209,43 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
bookingFields[name] = name;
|
bookingFields[name] = name;
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValues = {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
title: eventType.title,
|
const defaultValues: any = useMemo(() => {
|
||||||
locations: eventType.locations || [],
|
return {
|
||||||
recurringEvent: eventType.recurringEvent || null,
|
title: eventType.title,
|
||||||
description: eventType.description ?? undefined,
|
locations: eventType.locations || [],
|
||||||
schedule: eventType.schedule || undefined,
|
recurringEvent: eventType.recurringEvent || null,
|
||||||
bookingLimits: eventType.bookingLimits || undefined,
|
description: eventType.description ?? undefined,
|
||||||
durationLimits: eventType.durationLimits || undefined,
|
schedule: eventType.schedule || undefined,
|
||||||
length: eventType.length,
|
bookingLimits: eventType.bookingLimits || undefined,
|
||||||
offsetStart: eventType.offsetStart,
|
durationLimits: eventType.durationLimits || undefined,
|
||||||
hidden: eventType.hidden,
|
length: eventType.length,
|
||||||
periodDates: {
|
hidden: eventType.hidden,
|
||||||
startDate: periodDates.startDate,
|
periodDates: {
|
||||||
endDate: periodDates.endDate,
|
startDate: periodDates.startDate,
|
||||||
},
|
endDate: periodDates.endDate,
|
||||||
bookingFields: eventType.bookingFields,
|
|
||||||
periodType: eventType.periodType,
|
|
||||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
|
||||||
schedulingType: eventType.schedulingType,
|
|
||||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
|
||||||
metadata,
|
|
||||||
hosts: eventType.hosts,
|
|
||||||
children: eventType.children.map((ch) => ({
|
|
||||||
...ch,
|
|
||||||
created: true,
|
|
||||||
owner: {
|
|
||||||
...ch.owner,
|
|
||||||
eventTypeSlugs:
|
|
||||||
eventType.team?.members
|
|
||||||
.find((mem) => mem.user.id === ch.owner.id)
|
|
||||||
?.user.eventTypes.map((evTy) => evTy.slug)
|
|
||||||
.filter((slug) => slug !== eventType.slug) ?? [],
|
|
||||||
},
|
},
|
||||||
})),
|
bookingFields: eventType.bookingFields,
|
||||||
} as const;
|
periodType: eventType.periodType,
|
||||||
|
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||||
|
schedulingType: eventType.schedulingType,
|
||||||
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||||
|
metadata,
|
||||||
|
hosts: eventType.hosts,
|
||||||
|
children: eventType.children.map((ch) => ({
|
||||||
|
...ch,
|
||||||
|
created: true,
|
||||||
|
owner: {
|
||||||
|
...ch.owner,
|
||||||
|
eventTypeSlugs:
|
||||||
|
eventType.team?.members
|
||||||
|
.find((mem) => mem.user.id === ch.owner.id)
|
||||||
|
?.user.eventTypes.map((evTy) => evTy.slug)
|
||||||
|
.filter((slug) => slug !== eventType.slug) ?? [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}, [eventType, periodDates, metadata]);
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -270,6 +278,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
//TODO: What's the best way to sync the form with backend
|
//TODO: What's the best way to sync the form with backend
|
||||||
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||||
@@ -332,6 +341,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
seatsPerTimeSlotEnabled,
|
seatsPerTimeSlotEnabled,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
minimumBookingNoticeInDurationType,
|
minimumBookingNoticeInDurationType,
|
||||||
|
bookerLayouts,
|
||||||
...input
|
...input
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
@@ -345,6 +355,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
|
||||||
|
if (layoutError) throw new Error(t(layoutError));
|
||||||
|
|
||||||
if (metadata?.multipleDuration !== undefined) {
|
if (metadata?.multipleDuration !== undefined) {
|
||||||
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"));
|
||||||
@@ -359,8 +372,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
throw new Error(t("seats_and_no_show_fee_error"));
|
throw new Error(t("seats_and_no_show_fee_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { availability, ...rest } = input;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...input,
|
...rest,
|
||||||
locations,
|
locations,
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
periodStartDate: periodDates.startDate,
|
periodStartDate: periodDates.startDate,
|
||||||
@@ -415,9 +429,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
// 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,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
minimumBookingNoticeInDurationType,
|
|
||||||
availability,
|
|
||||||
...input
|
...input
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
@@ -431,6 +442,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
|
||||||
|
if (layoutError) throw new Error(t(layoutError));
|
||||||
|
|
||||||
if (metadata?.multipleDuration !== undefined) {
|
if (metadata?.multipleDuration !== undefined) {
|
||||||
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"));
|
||||||
@@ -440,9 +454,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { availability, ...rest } = input;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...input,
|
...rest,
|
||||||
locations,
|
locations,
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
periodStartDate: periodDates.startDate,
|
periodStartDate: periodDates.startDate,
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import type { FC } from "react";
|
|||||||
import { useEffect, useState, memo } from "react";
|
import { useEffect, useState, memo } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||||
|
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||||
|
import { OrganizationEventTypeFilter } from "@calcom/features/eventtypes/components/OrganizationEventTypeFilter";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
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";
|
||||||
@@ -44,6 +47,8 @@ import {
|
|||||||
HeadSeo,
|
HeadSeo,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Label,
|
Label,
|
||||||
|
VerticalDivider,
|
||||||
|
Alert,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
@@ -59,9 +64,11 @@ import {
|
|||||||
Trash,
|
Trash,
|
||||||
Upload,
|
Upload,
|
||||||
Users,
|
Users,
|
||||||
|
User as UserIcon,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
@@ -74,6 +81,7 @@ interface EventTypeListHeadingProps {
|
|||||||
profile: EventTypeGroupProfile;
|
profile: EventTypeGroupProfile;
|
||||||
membershipCount: number;
|
membershipCount: number;
|
||||||
teamId?: number | null;
|
teamId?: number | null;
|
||||||
|
orgSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventTypeGroup = EventTypeGroups[number];
|
type EventTypeGroup = EventTypeGroups[number];
|
||||||
@@ -194,6 +202,7 @@ const MemoizedItem = memo(Item);
|
|||||||
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
|
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const orgBranding = useOrgBrandingValues();
|
||||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||||
@@ -362,7 +371,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
|
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
|
||||||
{types.map((type, index) => {
|
{types.map((type, index) => {
|
||||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||||
const calLink = `${CAL_URL}/${embedLink}`;
|
const calLink = `${
|
||||||
|
orgBranding ? `${new URL(CAL_URL).protocol}//${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL
|
||||||
|
}/${embedLink}`;
|
||||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||||
const isChildrenManagedEventType =
|
const isChildrenManagedEventType =
|
||||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||||
@@ -393,11 +404,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
className="relative top-1 right-3"
|
className="relative top-1 right-3"
|
||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
items={type.users.map((organizer: { name: any; username: any }) => ({
|
items={type.users.map(
|
||||||
alt: organizer.name || "",
|
(organizer: { name: string | null; username: string | null }) => ({
|
||||||
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
alt: organizer.name || "",
|
||||||
title: organizer.name || "",
|
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
||||||
}))}
|
title: organizer.name || "",
|
||||||
|
})
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isManagedEventType && (
|
{isManagedEventType && (
|
||||||
@@ -685,6 +698,7 @@ const EventTypeListHeading = ({
|
|||||||
profile,
|
profile,
|
||||||
membershipCount,
|
membershipCount,
|
||||||
teamId,
|
teamId,
|
||||||
|
orgSlug,
|
||||||
}: EventTypeListHeadingProps): JSX.Element => {
|
}: EventTypeListHeadingProps): JSX.Element => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -725,7 +739,9 @@ const EventTypeListHeading = ({
|
|||||||
)}
|
)}
|
||||||
{profile?.slug && (
|
{profile?.slug && (
|
||||||
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
||||||
{`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
|
{orgSlug
|
||||||
|
? `${orgSlug}.${subdomainSuffix()}/${profile.slug}`
|
||||||
|
: `${CAL_URL?.replace("https://", "")}/${profile.slug}`}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -773,6 +789,7 @@ const CTA = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateButton
|
<CreateButton
|
||||||
|
data-testid="new-event-type"
|
||||||
subtitle={t("create_event_on").toUpperCase()}
|
subtitle={t("create_event_on").toUpperCase()}
|
||||||
options={profileOptions}
|
options={profileOptions}
|
||||||
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||||
@@ -780,19 +797,70 @@ const CTA = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
|
const Actions = () => {
|
||||||
|
return (
|
||||||
|
<div className="hidden items-center md:flex">
|
||||||
|
<OrganizationEventTypeFilter />
|
||||||
|
<VerticalDivider />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const orgBranding = useOrgBrandingValues();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
className="my-4"
|
||||||
|
severity="info"
|
||||||
|
title={t("set_up_your_profile")}
|
||||||
|
message={t("set_up_your_profile_description", { orgName: orgBranding?.name })}
|
||||||
|
CustomIcon={UserIcon}
|
||||||
|
actions={
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button color="minimal" className="text-sky-700 hover:bg-sky-100" onClick={closeAction}>
|
||||||
|
{t("dismiss")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="border-sky-700 bg-sky-50 text-sky-700 hover:border-sky-900 hover:bg-sky-200"
|
||||||
|
href="/getting-started">
|
||||||
|
{t("set_up")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any);
|
||||||
|
|
||||||
const EventTypesPage = () => {
|
const EventTypesPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { open } = useIntercom();
|
const { open } = useIntercom();
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
|
const { data: user } = useMeQuery();
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
const [showProfileBanner, setShowProfileBanner] = useState(false);
|
||||||
|
const orgBranding = useOrgBrandingValues();
|
||||||
|
|
||||||
|
function closeBanner() {
|
||||||
|
setShowProfileBanner(false);
|
||||||
|
document.cookie = `calcom-profile-banner=1;max-age=${60 * 60 * 24 * 90}`; // 3 months
|
||||||
|
showToast(t("we_wont_show_again"), "success");
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query?.openIntercom && query?.openIntercom === "true") {
|
if (query?.openIntercom && query?.openIntercom === "true") {
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
|
setShowProfileBanner(
|
||||||
|
!!orgBranding && !document.cookie.includes("calcom-profile-banner=1") && !user?.completedOnboarding
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -806,6 +874,8 @@ const EventTypesPage = () => {
|
|||||||
heading={t("event_types_page_title")}
|
heading={t("event_types_page_title")}
|
||||||
hideHeadingOnMobile
|
hideHeadingOnMobile
|
||||||
subtitle={t("event_types_page_subtitle")}
|
subtitle={t("event_types_page_subtitle")}
|
||||||
|
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
|
||||||
|
beforeCTAactions={<Actions />}
|
||||||
CTA={<CTA />}>
|
CTA={<CTA />}>
|
||||||
<WithQuery
|
<WithQuery
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader />}
|
||||||
@@ -816,12 +886,13 @@ const EventTypesPage = () => {
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||||
) : (
|
) : (
|
||||||
data.eventTypeGroups.map((group, index) => (
|
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||||
<div className="flex flex-col" key={group.profile.slug}>
|
<div className="flex flex-col" key={group.profile.slug}>
|
||||||
<EventTypeListHeading
|
<EventTypeListHeading
|
||||||
profile={group.profile}
|
profile={group.profile}
|
||||||
membershipCount={group.metadata.membershipCount}
|
membershipCount={group.metadata.membershipCount}
|
||||||
teamId={group.teamId}
|
teamId={group.teamId}
|
||||||
|
orgSlug={orgBranding?.slug}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EventTypeList
|
<EventTypeList
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
import { Suspense } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Button, StepCard, Steps } from "@calcom/ui";
|
import { Button, StepCard, Steps } from "@calcom/ui";
|
||||||
|
import { Loader } from "@calcom/ui/components/icon";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||||
@@ -20,7 +21,7 @@ import { SetupAvailability } from "@components/getting-started/steps-views/Setup
|
|||||||
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";
|
||||||
|
|
||||||
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const INITIAL_STEP = "user-settings";
|
const INITIAL_STEP = "user-settings";
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -44,9 +45,9 @@ const stepRouteSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
|
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
|
||||||
const OnboardingPage = (props: IOnboardingPageProps) => {
|
const OnboardingPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user } = props;
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const result = stepRouteSchema.safeParse(router.query);
|
const result = stepRouteSchema.safeParse(router.query);
|
||||||
@@ -117,9 +118,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
}
|
}
|
||||||
key={router.asPath}>
|
key={router.asPath}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{`${APP_NAME} - ${t("getting_started")}`}</title>
|
||||||
{APP_NAME} - {t("getting_started")}
|
|
||||||
</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
@@ -141,17 +140,20 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
|
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
|
||||||
</div>
|
</div>
|
||||||
<StepCard>
|
<StepCard>
|
||||||
{currentStep === "user-settings" && <UserSettings user={user} nextStep={() => goToIndex(1)} />}
|
<Suspense fallback={<Loader />}>
|
||||||
|
{currentStep === "user-settings" && <UserSettings nextStep={() => goToIndex(1)} />}
|
||||||
|
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
||||||
|
|
||||||
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
|
||||||
|
|
||||||
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
|
{currentStep === "setup-availability" && (
|
||||||
|
<SetupAvailability
|
||||||
{currentStep === "setup-availability" && (
|
nextStep={() => goToIndex(4)}
|
||||||
<SetupAvailability nextStep={() => goToIndex(4)} defaultScheduleId={user.defaultScheduleId} />
|
defaultScheduleId={user.defaultScheduleId}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
{currentStep === "user-profile" && <UserProfile user={user} />}
|
{currentStep === "user-profile" && <UserProfile />}
|
||||||
|
</Suspense>
|
||||||
</StepCard>
|
</StepCard>
|
||||||
|
|
||||||
{headers[currentStepIndex]?.skipText && (
|
{headers[currentStepIndex]?.skipText && (
|
||||||
@@ -178,34 +180,21 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
||||||
const crypto = await import("crypto");
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
await ssr.viewer.me.prefetch();
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
timeZone: true,
|
|
||||||
weekStart: true,
|
|
||||||
hideBranding: true,
|
|
||||||
theme: true,
|
|
||||||
brandColor: true,
|
|
||||||
darkBrandColor: true,
|
|
||||||
metadata: true,
|
|
||||||
timeFormat: true,
|
|
||||||
allowDynamicBooking: true,
|
|
||||||
defaultScheduleId: true,
|
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
teams: {
|
teams: {
|
||||||
select: {
|
select: {
|
||||||
@@ -233,10 +222,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(context.locale ?? "", ["common"])),
|
...(await serverSideTranslations(context.locale ?? "", ["common"])),
|
||||||
user: {
|
trpcState: ssr.dehydrate(),
|
||||||
...user,
|
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
|
||||||
},
|
|
||||||
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Heading = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="min-w-52 hidden md:block">
|
<div className="min-w-52 hidden md:block">
|
||||||
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 text-emphasis truncate text-xl font-semibold tracking-wide xl:max-w-full">
|
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 text-emphasis truncate text-xl font-semibold tracking-wide xl:max-w-full">
|
||||||
{t("analytics_for_organisation")}
|
{t("insights")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-default hidden text-sm md:block">{t("subtitle_analytics")}</p>
|
<p className="text-default hidden text-sm md:block">{t("subtitle_analytics")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Booker } from "@calcom/atoms";
|
|||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -15,8 +17,9 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
|
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||||
return (
|
return (
|
||||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
|
||||||
<BookerSeo
|
<BookerSeo
|
||||||
username={user}
|
username={user}
|
||||||
eventSlug={slug}
|
eventSlug={slug}
|
||||||
@@ -84,6 +87,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
away: false,
|
away: false,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: false,
|
isBrandingHidden: false,
|
||||||
|
themeBasis: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,12 +95,18 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid } = context.query;
|
const { rescheduleUid } = context.query;
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
away: true,
|
away: true,
|
||||||
@@ -133,6 +143,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
slug,
|
slug,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: user?.hideBranding,
|
isBrandingHidden: user?.hideBranding,
|
||||||
|
themeBasis: username,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -143,7 +154,7 @@ const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
|||||||
// whether the page should show an away state or dynamic booking not allowed.
|
// whether the page should show an away state or dynamic booking not allowed.
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { user } = paramsSchema.parse(context.params);
|
const { user } = paramsSchema.parse(context.params);
|
||||||
const isDynamicGroup = user.includes("+");
|
const isDynamicGroup = getUsernameList(user).length > 1;
|
||||||
|
|
||||||
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||||
|
|
||||||
|
export { default } from "../[type]";
|
||||||
|
|
||||||
|
// Somehow these types don't accept the {notFound: true} return type.
|
||||||
|
// Probably still need to fix this. I don't know why this isn't allowed yet.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../../[user]";
|
||||||
|
|
||||||
|
export { default } from "../../[user]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
|
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
|
return (
|
||||||
|
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||||
|
<BookerSeo
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleUid={booking?.uid}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
|
<Booker
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleBooking={booking}
|
||||||
|
isAway={away}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
|
const { link, slug } = paramsSchema.parse(context.params);
|
||||||
|
const { rescheduleUid } = context.query;
|
||||||
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||||
|
|
||||||
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
const hashedLink = await prisma.hashedLink.findUnique({
|
||||||
|
where: {
|
||||||
|
link,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
users: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const username = hashedLink?.eventType.users[0]?.username;
|
||||||
|
|
||||||
|
if (!hashedLink || !username) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username,
|
||||||
|
organization: isValidOrgDomain
|
||||||
|
? {
|
||||||
|
slug: currentOrgDomain,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
away: true,
|
||||||
|
hideBranding: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let booking: GetBookingType | null = null;
|
||||||
|
if (rescheduleUid) {
|
||||||
|
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use this to both prefetch the query on the server,
|
||||||
|
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||||
|
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug });
|
||||||
|
|
||||||
|
if (!eventData) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
booking,
|
||||||
|
away: user?.away,
|
||||||
|
user: username,
|
||||||
|
slug,
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
isBrandingHidden: user?.hideBranding,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsSchema = z.object({ link: z.string(), slug: z.string() });
|
||||||
|
|
||||||
|
// Booker page fetches a tiny bit of data server side, to determine early
|
||||||
|
// whether the page should show an away state or dynamic booking not allowed.
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
return await getUserPageProps(context);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { Booker } from "@calcom/atoms";
|
|||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
@@ -14,8 +15,9 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
|
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||||
return (
|
return (
|
||||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
|
||||||
<BookerSeo
|
<BookerSeo
|
||||||
username={user}
|
username={user}
|
||||||
eventSlug={slug}
|
eventSlug={slug}
|
||||||
@@ -85,6 +87,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
slug: meetingSlug,
|
slug: meetingSlug,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: team?.hideBranding,
|
isBrandingHidden: team?.hideBranding,
|
||||||
|
themeBasis: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||||
|
|
||||||
|
export { default } from "../[type]";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../../../team/[slug]";
|
||||||
|
|
||||||
|
export { default } from "../../../team/[slug]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -47,15 +47,13 @@ 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 title={t("view_and_manage_billing_details")} description={t("view_and_edit_billing_details")}>
|
||||||
title={t("billing_manage_details_title")}
|
|
||||||
description={t("billing_manage_details_description")}>
|
|
||||||
<Button color="primary" href={billingHref} target="_blank" EndIcon={ExternalLink}>
|
<Button color="primary" href={billingHref} target="_blank" EndIcon={ExternalLink}>
|
||||||
{t("billing_portal")}
|
{t("billing_portal")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
|
|
||||||
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
|
||||||
<Button color="secondary" onClick={onContactSupportClick}>
|
<Button color="secondary" onClick={onContactSupportClick}>
|
||||||
{t("contact_support")}
|
{t("contact_support")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||||
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
|
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
|
||||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||||
|
import type { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -62,6 +66,7 @@ const AppearanceView = () => {
|
|||||||
brandColor: user?.brandColor || "#292929",
|
brandColor: user?.brandColor || "#292929",
|
||||||
darkBrandColor: user?.darkBrandColor || "#fafafa",
|
darkBrandColor: user?.darkBrandColor || "#fafafa",
|
||||||
hideBranding: user?.hideBranding,
|
hideBranding: user?.hideBranding,
|
||||||
|
metadata: user?.metadata as z.infer<typeof userMetadata>,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,8 +81,12 @@ const AppearanceView = () => {
|
|||||||
showToast(t("settings_updated_successfully"), "success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
reset(data);
|
reset(data);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error) => {
|
||||||
showToast(t("error_updating_settings"), "error");
|
if (error.message) {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
} else {
|
||||||
|
showToast(t("error_updating_settings"), "error");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +101,9 @@ const AppearanceView = () => {
|
|||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={(values) => {
|
handleSubmit={(values) => {
|
||||||
|
const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null);
|
||||||
|
if (layoutError) throw new Error(t(layoutError));
|
||||||
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...values,
|
...values,
|
||||||
// Radio values don't support null as values, therefore we convert an empty string
|
// Radio values don't support null as values, therefore we convert an empty string
|
||||||
@@ -117,19 +129,26 @@ const AppearanceView = () => {
|
|||||||
<ThemeLabel
|
<ThemeLabel
|
||||||
variant="light"
|
variant="light"
|
||||||
value="light"
|
value="light"
|
||||||
label={t("theme_light")}
|
label={t("light")}
|
||||||
defaultChecked={user.theme === "light"}
|
defaultChecked={user.theme === "light"}
|
||||||
register={formMethods.register}
|
register={formMethods.register}
|
||||||
/>
|
/>
|
||||||
<ThemeLabel
|
<ThemeLabel
|
||||||
variant="dark"
|
variant="dark"
|
||||||
value="dark"
|
value="dark"
|
||||||
label={t("theme_dark")}
|
label={t("dark")}
|
||||||
defaultChecked={user.theme === "dark"}
|
defaultChecked={user.theme === "dark"}
|
||||||
register={formMethods.register}
|
register={formMethods.register}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-subtle my-8 border [&:has(+hr)]:hidden" />
|
||||||
|
<BookerLayoutSelector
|
||||||
|
name="metadata.defaultBookerLayouts"
|
||||||
|
title={t("bookerlayout_user_settings_title")}
|
||||||
|
description={t("bookerlayout_user_settings_description")}
|
||||||
|
/>
|
||||||
|
|
||||||
<hr className="border-subtle my-8 border" />
|
<hr className="border-subtle my-8 border" />
|
||||||
<div className="mb-6 flex items-center text-sm">
|
<div className="mb-6 flex items-center text-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -244,7 +263,8 @@ const AppearanceView = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="mt-8">
|
className="mt-8"
|
||||||
|
data-testid="update-theme-btn">
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const ConferencingLayout = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{t("change_default_conferencing_app")}
|
{t("set_as_default")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ interface GeneralViewProps {
|
|||||||
user: RouterOutputs["viewer"]["me"];
|
user: RouterOutputs["viewer"]["me"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const WithQuery = withQuery(trpc.viewer.public.i18n, undefined, { trpc: { context: { skipBatch: true } } });
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const WithQuery = withQuery(trpc.viewer.public.i18n as any, undefined, {
|
||||||
|
trpc: { context: { skipBatch: true } },
|
||||||
|
});
|
||||||
|
|
||||||
const GeneralQueryView = () => {
|
const GeneralQueryView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ const ProfileView = () => {
|
|||||||
extraField={
|
extraField={
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<UsernameAvailabilityField
|
<UsernameAvailabilityField
|
||||||
user={user}
|
|
||||||
onSuccessMutation={async () => {
|
onSuccessMutation={async () => {
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
await utils.viewer.me.invalidate();
|
await utils.viewer.me.invalidate();
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { AboutOrganizationForm } from "@calcom/features/ee/organizations/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { WizardLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
const AboutOrganizationPage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
if (!router.isReady) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("about_your_organization")} description={t("about_your_organization_description")} />
|
||||||
|
<AboutOrganizationForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const LayoutWrapper = (page: React.ReactElement) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout currentStep={3} maxSteps={5}>
|
||||||
|
{page}
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AboutOrganizationPage.getLayout = LayoutWrapper;
|
||||||
|
AboutOrganizationPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
|
export default AboutOrganizationPage;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { NextRouter } from "next/router";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { WizardLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
const AddNewTeamsPage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
if (!router.isReady) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("create_your_teams")} description={t("create_your_teams_description")} />
|
||||||
|
<AddNewTeamsForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextRouter) => (
|
||||||
|
<>
|
||||||
|
<WizardLayout
|
||||||
|
currentStep={5}
|
||||||
|
maxSteps={5}
|
||||||
|
isOptionalCallback={() => {
|
||||||
|
router.push(`/event-types`);
|
||||||
|
}}>
|
||||||
|
{page}
|
||||||
|
</WizardLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
AddNewTeamsPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
|
export default AddNewTeamsPage;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { NextRouter } from "next/router";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { AddNewOrgAdminsForm } from "@calcom/features/ee/organizations/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { WizardLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
const OnboardTeamMembersPage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
if (!router.isReady) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta
|
||||||
|
title={t("invite_organization_admins")}
|
||||||
|
description={t("invite_organization_admins_description")}
|
||||||
|
/>
|
||||||
|
<AddNewOrgAdminsForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OnboardTeamMembersPage.getLayout = (page: React.ReactElement, router: NextRouter) => (
|
||||||
|
<WizardLayout
|
||||||
|
currentStep={4}
|
||||||
|
maxSteps={5}
|
||||||
|
isOptionalCallback={() => {
|
||||||
|
router.push(`/settings/organizations/${router.query.id}/add-teams`);
|
||||||
|
}}>
|
||||||
|
{page}
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
OnboardTeamMembersPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
|
export default OnboardTeamMembersPage;
|
||||||
Loaded 100 of 570 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user