Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df91dacc3f | ||
|
|
396b81af58 | ||
|
|
8467a9c53b | ||
|
|
e99e8b169a | ||
|
|
7b8902a769 | ||
|
|
2408338ed4 | ||
|
|
8f0ff58bcf | ||
|
|
fac47544d1 | ||
|
|
b09f3e5c8d | ||
|
|
8bb051ac4e | ||
|
|
fd3fffd7dd | ||
|
|
312a3f2543 | ||
|
|
f52f2f32fa | ||
|
|
fdd0a5a03d | ||
|
|
ca3e3600f7 | ||
|
|
ba944b471a | ||
|
|
7978f0317a | ||
|
|
d918f34ae8 | ||
|
|
1fed08fa9b | ||
|
|
aabfd3b3f6 | ||
|
|
60e0bbf4b7 | ||
|
|
0a7f65cd9b | ||
|
|
f41f4b01a9 | ||
|
|
a7b38b1b58 | ||
|
|
a0f1ceba19 | ||
|
|
5c6e104ff5 | ||
|
|
29b1f7bf51 | ||
|
|
7ec3d04acc | ||
|
|
13a765e50a | ||
|
|
18f637d94c | ||
|
|
e5eb121ab7 | ||
|
|
6612926aa5 | ||
|
|
93dc1247f1 | ||
|
|
24607873c0 | ||
|
|
8b37d91108 | ||
|
|
22b0644700 | ||
|
|
404a7c1550 | ||
|
|
19a2cd5ae4 | ||
|
|
0798e2940a | ||
|
|
b1bf94aa58 | ||
|
|
0a921c2929 | ||
|
|
67c318e527 | ||
|
|
41b8d2ab1a | ||
|
|
57124b48fe | ||
|
|
b76a2a4019 | ||
|
|
a28b9cacd2 | ||
|
|
abd90f6af8 | ||
|
|
0bdc45a1a5 | ||
|
|
65d9704f2b | ||
|
|
d8ba783369 | ||
|
|
782127a993 | ||
|
|
fbc3f7b51f | ||
|
|
619ccf4065 | ||
|
|
40d8f34e8d | ||
|
|
9c1e1d7312 | ||
|
|
710a1a7d38 | ||
|
|
f25605ef4d | ||
|
|
684575d0a4 | ||
|
|
bc6267e99b | ||
|
|
9901c91e9b | ||
|
|
dfe03efc37 | ||
|
|
0eabb56f07 | ||
|
|
cdd066c653 | ||
|
|
026f22fbaf | ||
|
|
5950c5a756 | ||
|
|
cd9d16be3e | ||
|
|
993f92acba | ||
|
|
6f110d21fd | ||
|
|
0df6777814 | ||
|
|
5f14cd31d1 | ||
|
|
070ec326aa | ||
|
|
4ca79af13f | ||
|
|
a2e70f9aad | ||
|
|
f186d2e41d | ||
|
|
aaeea250bc | ||
|
|
96af17d8d7 | ||
|
|
dfaa6d28e4 | ||
|
|
6a2de993bb | ||
|
|
b69d885ee6 | ||
|
|
d001c4e2ae | ||
|
|
558f1c9478 | ||
|
|
74748a6183 | ||
|
|
986f17f54b | ||
|
|
ab342016d2 | ||
|
|
c34c27fc0a | ||
|
|
6dbf372ab0 | ||
|
|
7dc7f949cf | ||
|
|
5690718e7f | ||
|
|
97d5bb9fe6 | ||
|
|
fcc50c1d0f | ||
|
|
b832289f8e | ||
|
|
4aa0b4cc65 | ||
|
|
e2ef9dd710 | ||
|
|
15b0e0c5bd | ||
|
|
020515de8c | ||
|
|
87cb8e918f | ||
|
|
69677495ca | ||
|
|
a40520b90c | ||
|
|
c07b456b2d | ||
|
|
7ec2a4b8fa | ||
|
|
dccaf5fb67 | ||
|
|
d76060e037 | ||
|
|
48d4725e10 | ||
|
|
2f1e545976 | ||
|
|
50fb903ba6 | ||
|
|
ccda3fd71e | ||
|
|
c44fb074ab | ||
|
|
d56d649bf0 | ||
|
|
8856ba7d70 | ||
|
|
0897bf20da | ||
|
|
6ce6d570db | ||
|
|
b99ccb1a5a | ||
|
|
698d8ae4bd | ||
|
|
0dddc2224a | ||
|
|
49f9f5489b | ||
|
|
ecb693c70e | ||
|
|
3c6fdfe724 | ||
|
|
ef7f0e2259 | ||
|
|
3791af8644 | ||
|
|
c19799e275 | ||
|
|
4c4fc9e38b | ||
|
|
2220778e6b | ||
|
|
0f707a55b0 | ||
|
|
de1c9d01cd | ||
|
|
6a1325867e | ||
|
|
574a4a847d | ||
|
|
f201266d69 | ||
|
|
71e57bafde | ||
|
|
c4b296d580 | ||
|
|
7d7e74c869 | ||
|
|
6dec98129c | ||
|
|
c7b62950de | ||
|
|
4dba72fecb | ||
|
|
e61be783d1 | ||
|
|
299a866aac | ||
|
|
e39e6ccf79 | ||
|
|
218f6a84b9 | ||
|
|
1a3e4d10c0 | ||
|
|
f848a44f1a | ||
|
|
2181731d64 | ||
|
|
8c8401330a | ||
|
|
7015c8909f | ||
|
|
cbee4ff704 | ||
|
|
ffefb3461e | ||
|
|
1c2fff5447 | ||
|
|
6f942cfcac | ||
|
|
7579417d7e | ||
|
|
16d1adf990 | ||
|
|
bdc36acdf5 | ||
|
|
1f036bf35e | ||
|
|
5de77e386c | ||
|
|
412e7ecbce | ||
|
|
55c9efec3e | ||
|
|
c4792c55fe | ||
|
|
04cad3a69e | ||
|
|
f7b257750a | ||
|
|
4fa7bb64eb | ||
|
|
69656b7861 | ||
|
|
b543c4030c | ||
|
|
4ce62b84ce | ||
|
|
076868d243 | ||
|
|
a03a1ba34e | ||
|
|
dea873fef1 | ||
|
|
9fbb0808be | ||
|
|
beae1aae8b | ||
|
|
e56b4b57bf | ||
|
|
4faad64978 | ||
|
|
b384e4a9d8 | ||
|
|
d07e86e4f3 | ||
|
|
68d40cabbe | ||
|
|
f07d0479d4 | ||
|
|
9a2c2aaca4 | ||
|
|
2693b30c51 | ||
|
|
95cc0eeaae | ||
|
|
b49317a83f | ||
|
|
24264c32dc | ||
|
|
e248dfbda7 | ||
|
|
31c0804386 | ||
|
|
a3443b4e76 | ||
|
|
254dfc9e6a | ||
|
|
ef6bf3d451 | ||
|
|
4b3ad579c8 | ||
|
|
27ce5e7808 | ||
|
|
4d99a7fbdf | ||
|
|
27541227c8 | ||
|
|
83a187bf11 | ||
|
|
200ce6932d | ||
|
|
6d5983fabc | ||
|
|
18b994bb4c | ||
|
|
a8975f541f | ||
|
|
31b88c5537 | ||
|
|
f0bd628713 | ||
|
|
260bd4829c | ||
|
|
dc8731d64f | ||
|
|
48eba5ef5c | ||
|
|
544d76d503 | ||
|
|
6ce2d9819e | ||
|
|
2e0c9c7068 | ||
|
|
038008502c | ||
|
|
5886792285 | ||
|
|
e3905f631f | ||
|
|
e5e0fa97eb | ||
|
|
f1caaad536 | ||
|
|
b6279f4128 | ||
|
|
5fb1857538 | ||
|
|
a8d9b0210a | ||
|
|
5751f0f634 | ||
|
|
4b16860d07 | ||
|
|
f77e480d02 | ||
|
|
099f36deeb | ||
|
|
09f804413b | ||
|
|
e1a9576530 | ||
|
|
2410f8b3aa | ||
|
|
a28e8ff39b | ||
|
|
7d2500a32f | ||
|
|
22d906798e | ||
|
|
f71759f70b | ||
|
|
a323e06e88 | ||
|
|
b4db3a75a8 | ||
|
|
a9fcd900cd | ||
|
|
fcb443a8eb | ||
|
|
9dfa596e3e | ||
|
|
0e8ac7e4ed | ||
|
|
b236bffca2 | ||
|
|
6e829e23f9 | ||
|
|
b778b2962a | ||
|
|
0d3baee593 | ||
|
|
f9dcbaaa42 | ||
|
|
60700e1a7a | ||
|
|
456adea570 | ||
|
|
63abda1693 | ||
|
|
4dd73f4d86 | ||
|
|
31caf8288d | ||
|
|
99e71365ab | ||
|
|
943c7a4c6c | ||
|
|
228e509a3b | ||
|
|
eb909cc87a | ||
|
|
65a36b37c5 | ||
|
|
2799ddf3a4 | ||
|
|
3164cd4ae7 | ||
|
|
6b26dbc6da | ||
|
|
6b09c6b3b4 | ||
|
|
2352bb1075 | ||
|
|
a568e1ac66 | ||
|
|
add6ffdfc4 | ||
|
|
7a67331d96 | ||
|
|
1aa8b8439a | ||
|
|
50e405353c | ||
|
|
565e21bc50 | ||
|
|
098d41fc62 | ||
|
|
a20179727b | ||
|
|
795aaca64f | ||
|
|
2ab7846a4a | ||
|
|
e9ea0fcc20 | ||
|
|
090b166b1c | ||
|
|
ea2d8fc873 | ||
|
|
46c51df1e0 | ||
|
|
5fe919452b | ||
|
|
83c8f97afd | ||
|
|
f87eac193f | ||
|
|
c53e891c8a | ||
|
|
90a6fc3f26 | ||
|
|
969411041b | ||
|
|
518cfbc037 | ||
|
|
47277ced2d | ||
|
|
db625157d1 | ||
|
|
bf2af799d5 | ||
|
|
75eaed1c4d | ||
|
|
e1ac6f5454 | ||
|
|
55d44ce789 | ||
|
|
8bed690ed6 | ||
|
|
adf012643a | ||
|
|
ca0d23529a | ||
|
|
9f50941904 | ||
|
|
f2a59fe4e8 | ||
|
|
325f250d39 | ||
|
|
bb7057ea04 | ||
|
|
d185d7f7d8 | ||
|
|
80b92b5f11 | ||
|
|
9a6d4e63e8 | ||
|
|
4062ae8486 | ||
|
|
c6f26f2a2a | ||
|
|
2f4b1818d0 | ||
|
|
8e1ce633fb | ||
|
|
44e4f4c644 | ||
|
|
0b9cf78e9b | ||
|
|
b2cdb08780 | ||
|
|
dcf9c6a5e5 | ||
|
|
ca78be011c | ||
|
|
d13dedda9a | ||
|
|
9ca2ad2d41 | ||
|
|
3d89809e67 | ||
|
|
252ce07861 | ||
|
|
a75ef8870b | ||
|
|
b840b3b1bb | ||
|
|
2e50625165 | ||
|
|
c78eb752a7 | ||
|
|
1c20bdcecf | ||
|
|
338756439f | ||
|
|
56050b994d | ||
|
|
c11ecbb323 | ||
|
|
7f23ae156b | ||
|
|
33250652b3 | ||
|
|
a65e18d92d | ||
|
|
d0f7085cb8 | ||
|
|
ea8437b4f7 | ||
|
|
877cd4cdff | ||
|
|
bae3bd76e5 | ||
|
|
8b24995d52 | ||
|
|
7dc6df2ad6 | ||
|
|
6c4b1154b8 | ||
|
|
1099bad930 | ||
|
|
dbd5d27d9d | ||
|
|
45c4f4789b | ||
|
|
07f42057fc | ||
|
|
79ee7c41b2 | ||
|
|
57e6971942 | ||
|
|
bd6ca21e02 | ||
|
|
f19a1926b4 |
@@ -37,6 +37,7 @@ BASECAMP3_USER_AGENT=
|
||||
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
DAILY_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
@@ -126,4 +127,12 @@ ZOHOCRM_CLIENT_ID=""
|
||||
ZOHOCRM_CLIENT_SECRET=""
|
||||
|
||||
|
||||
# - REVERT
|
||||
# Used for the Pipedrive integration (via/ Revert (https://revert.dev))
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-revert-api-keys
|
||||
REVERT_API_KEY=
|
||||
REVERT_PUBLIC_TOKEN=
|
||||
|
||||
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
|
||||
REVERT_API_URL=https://api.revert.dev/
|
||||
# *********************************************************************************************************
|
||||
|
||||
+43
-2
@@ -133,6 +133,11 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||
# Used for capturing exceptions and logging messages
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Formbricks Experience Management Integration
|
||||
FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||
FORMBRICKS_ENVIRONMENT_ID=
|
||||
FORMBRICKS_FEEDBACK_SURVEY_ID=
|
||||
|
||||
# Twilio
|
||||
# Used to send SMS reminders in workflows
|
||||
TWILIO_SID=
|
||||
@@ -196,6 +201,10 @@ EMAIL_SERVER_PORT=1025
|
||||
# Make sure to run mailhog container manually or with `yarn dx`
|
||||
E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# Resend
|
||||
# Send transactional email using resend
|
||||
# RESEND_API_KEY=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
@@ -245,6 +254,18 @@ PROJECT_ID_VERCEL=
|
||||
TEAM_ID_VERCEL=
|
||||
# Get it from: https://vercel.com/account/tokens
|
||||
AUTH_BEARER_TOKEN_VERCEL=
|
||||
# Add the main domain that you want to use for testing vercel domain management for organizations. This is necessary because WEBAPP_URL of local isn't a valid public domain
|
||||
# Would create org1.example.com for an org with slug org1
|
||||
# LOCAL_TESTING_DOMAIN_VERCEL="example.com"
|
||||
|
||||
## Set it to 1 if you use cloudflare to manage your DNS and would like us to manage the DNS for you for organizations
|
||||
# CLOUDFLARE_DNS=1
|
||||
## Get it from: https://dash.cloudflare.com/profile/api-tokens. Select Edit Zone template and choose a zone(your domain)
|
||||
# AUTH_BEARER_TOKEN_CLOUDFLARE=
|
||||
## Zone ID can be found in the Overview tab of your domain in Cloudflare
|
||||
# CLOUDFLARE_ZONE_ID=
|
||||
## It should usually work with the default value. This is the DNS CNAME record content to point to Vercel domain
|
||||
# CLOUDFLARE_VERCEL_CNAME=cname.vercel-dns.com
|
||||
|
||||
# - APPLE CALENDAR
|
||||
# Used for E2E tests on Apple Calendar
|
||||
@@ -256,6 +277,8 @@ E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS=
|
||||
E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
@@ -266,7 +289,7 @@ CALCOM_WEBHOOK_SECRET=""
|
||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 32 bytes for AES256 encryption algorithm
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate one
|
||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||
|
||||
@@ -289,4 +312,22 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_VIDEO_ENABLED=0
|
||||
APP_ROUTER_TEAMS_ENABLED=0
|
||||
|
||||
# disable setry server source maps
|
||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
|
||||
@@ -19,11 +19,12 @@ Fixes # (issue)
|
||||
|
||||
<!-- Please delete bullets that are not relevant. -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- Bug fix (non-breaking change which fixes an issue)
|
||||
- Chore (refactoring code, technical debt, workflow improvements)
|
||||
- New feature (non-breaking change which adds functionality)
|
||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- Tests (Unit/Integration/E2E or any other test)
|
||||
- This change requires a documentation update
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ runs:
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: yarn build
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn build
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
||||
@@ -17,10 +17,14 @@ runs:
|
||||
cache-name: cache-db
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||
- run: yarn db-seed
|
||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: buildjet/cache@v2
|
||||
uses: buildjet/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
name: "Apply issue labels to PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: Check types
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
jobs:
|
||||
check-types:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
@@ -17,10 +17,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: 7
|
||||
days-before-pr-close: -1
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: E2E App-Store Apps
|
||||
name: E2E App-Store Apps Tests
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
jobs:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
@@ -29,10 +30,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
@@ -43,6 +49,10 @@ jobs:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
@@ -65,7 +75,7 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: E2E Embed React tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed React tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
@@ -24,7 +25,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: E2E Embed Core tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed Core tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
@@ -29,7 +30,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: E2E test
|
||||
|
||||
name: E2E tests
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
@@ -28,7 +28,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
@@ -68,7 +67,7 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
pull_request_target:
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -16,3 +17,81 @@ jobs:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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,7 @@ jobs:
|
||||
|
||||
- name: Upload ESLint report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lint-results
|
||||
path: lint-results
|
||||
|
||||
@@ -2,6 +2,7 @@ name: "Next.js Bundle Analysis"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -27,14 +28,14 @@ jobs:
|
||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bundle
|
||||
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||
|
||||
- name: Download base branch bundle stats
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
if: success() && github.event.number
|
||||
if: success()
|
||||
with:
|
||||
workflow: nextjs-bundle-analysis.yml
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
@@ -54,7 +55,7 @@ jobs:
|
||||
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||
# entry in your package.json file.
|
||||
- name: Compare with base branch bundle
|
||||
if: success() && github.event.number
|
||||
if: success()
|
||||
run: |
|
||||
cd apps/web
|
||||
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||
@@ -71,7 +72,7 @@ jobs:
|
||||
echo ::set-output name=body::$body
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
uses: peter-evans/find-comment@v2
|
||||
if: success() && github.event.number
|
||||
id: fc
|
||||
with:
|
||||
@@ -79,14 +80,14 @@ jobs:
|
||||
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||
|
||||
- name: Create Comment
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: ${{ steps.get-comment-body.outputs.body }}
|
||||
|
||||
- name: Update Comment
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Assign PR team labels
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
team-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
+31
-23
@@ -4,9 +4,6 @@ on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -15,15 +12,21 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
login:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -31,78 +34,83 @@ jobs:
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
type-check:
|
||||
name: Type check
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/test.yml
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
name: Analyze Build
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||
run: exit 1
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Pre-release checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -9,6 +9,10 @@ env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -44,5 +44,5 @@ jobs:
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
|
||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
- run: yarn test
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
|
||||
+11
-11
@@ -1,15 +1,15 @@
|
||||
diff --git a/index.cjs b/index.cjs
|
||||
index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644
|
||||
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
|
||||
--- a/index.cjs
|
||||
+++ b/index.cjs
|
||||
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
||||
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
|
||||
// An alternative approach:
|
||||
// https://www.npmjs.com/package/babel-plugin-add-module-exports
|
||||
-exports = module.exports = min.parsePhoneNumberFromString
|
||||
-exports['default'] = min.parsePhoneNumberFromString
|
||||
+// exports = module.exports = min.parsePhoneNumberFromString
|
||||
+// exports['default'] = min.parsePhoneNumberFromString
|
||||
|
||||
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
||||
// it has been promoted to a default export due to being too verbose.
|
||||
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
|
||||
// An alternative approach:
|
||||
// https://www.npmjs.com/package/babel-plugin-add-module-exports
|
||||
-exports = module.exports = min.parsePhoneNumberFromString
|
||||
-exports['default'] = min.parsePhoneNumberFromString
|
||||
+// exports = module.exports = min.parsePhoneNumberFromString
|
||||
+// exports['default'] = min.parsePhoneNumberFromString
|
||||
|
||||
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
||||
// it has been promoted to a default export due to being too verbose.
|
||||
@@ -7,7 +7,7 @@
|
||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||
|
||||
<p align="center">
|
||||
The open-source Calendly alternative.
|
||||
The open-source Calendly successor.
|
||||
<br />
|
||||
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
||||
<br />
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
# Scheduling infrastructure for absolutely everyone
|
||||
|
||||
The open source Calendly alternative. You are in charge
|
||||
The open source Calendly successor. You are in charge
|
||||
of your own data, workflow, and appearance.
|
||||
|
||||
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
|
||||
@@ -147,7 +147,7 @@ Here is what you need to be able to run Cal.com.
|
||||
|
||||
- Duplicate `.env.example` to `.env`
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
5. Setup Node
|
||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||
@@ -216,12 +216,11 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||
|
||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||
|
||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||
|
||||
1. Set a 24 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`)
|
||||
|
||||
In a development environment, run:
|
||||
@@ -449,7 +448,7 @@ Don't code but still want to contribute? Join our [discord](https://go.cal.com/d
|
||||
3. Enable the selected API.
|
||||
4. Next, go to the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) from the side pane. Select the app type (Internal or External) and enter the basic app details on the first page.
|
||||
5. In the second page on Scopes, select Add or Remove Scopes. Search for Calendar.event and select the scope with scope value `.../auth/calendar.events`, `.../auth/calendar.readonly` and select Update.
|
||||
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
||||
6. In the third page (Test Users), add the Google account(s) you'll be using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
||||
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
|
||||
8. Select Web Application as the Application Type.
|
||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
|
||||
@@ -555,6 +554,10 @@ following
|
||||
|
||||
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||
|
||||
### Obtaining Pipedrive Client ID and Secret
|
||||
|
||||
[Follow these steps](./packages/app-store/pipedrive-crm/)
|
||||
|
||||
## Workflows
|
||||
|
||||
### Setting up SendGrid for Email reminders
|
||||
|
||||
@@ -41,6 +41,28 @@ test.describe("Org", () => {
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
});
|
||||
test.describe("Dynamic Group Booking", () => {
|
||||
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||
const users = [
|
||||
{
|
||||
username: "peer",
|
||||
name: "Peer Richelsen",
|
||||
},
|
||||
{
|
||||
username: "bailey",
|
||||
name: "Bailey Pumfleet",
|
||||
},
|
||||
];
|
||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||
// 2 users and 1 for the organization(2+1)
|
||||
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
|
||||
+8
-4
@@ -48,17 +48,21 @@ Here is the full architecture:
|
||||
|
||||
### Email Router
|
||||
|
||||
To expose the AI app, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
|
||||
To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool.
|
||||
|
||||
For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe))
|
||||
|
||||
For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first.
|
||||
|
||||
To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
|
||||
|
||||
1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/)
|
||||
2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication).
|
||||
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`, both with priority `10` if prompted.
|
||||
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted.
|
||||
4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain.
|
||||
5. In the Destination URL field, use the nGrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
|
||||
5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
|
||||
6. Activate "POST the raw, full MIME message".
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the nGrok listener and server.
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
|
||||
8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
|
||||
|
||||
Please feel free to improve any part of this architecture!
|
||||
|
||||
@@ -22,6 +22,8 @@ const fetchBookings = async ({
|
||||
const params = {
|
||||
apiKey,
|
||||
userId: userId.toString(),
|
||||
dateFrom: from,
|
||||
dateTo: to,
|
||||
};
|
||||
|
||||
const urlParams = new URLSearchParams(params);
|
||||
@@ -40,11 +42,9 @@ const fetchBookings = async ({
|
||||
|
||||
const bookings: Booking[] = data.bookings
|
||||
.filter((booking: Booking) => {
|
||||
const afterFrom = new Date(booking.startTime).getTime() > new Date(from).getTime();
|
||||
const beforeTo = new Date(booking.endTime).getTime() < new Date(to).getTime();
|
||||
const notCancelled = booking.status !== BOOKING_STATUS.CANCELLED;
|
||||
|
||||
return afterFrom && beforeTo && notCancelled;
|
||||
return notCancelled;
|
||||
})
|
||||
.map(({ endTime, eventTypeId, id, startTime, status, title }: Booking) => ({
|
||||
endTime,
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { User, UserList } from "../types/user";
|
||||
import type { WorkingHours } from "../types/workingHours";
|
||||
import now from "./now";
|
||||
|
||||
const gptModel = "gpt-4";
|
||||
const gptModel = "gpt-4-0613";
|
||||
|
||||
/**
|
||||
* Core of the Cal.ai booking agent: a LangChain Agent Executor.
|
||||
@@ -58,7 +58,14 @@ If you can't find a referenced user, ask the user for their email or @username.
|
||||
|
||||
The primary user's id is: ${userId}
|
||||
The primary user's username is: ${user.username}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone)}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
The primary user's time zone is: ${user.timeZone}
|
||||
The primary user's event types are: ${user.eventTypes
|
||||
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default function now(timeZone: string) {
|
||||
export default function now(timeZone: string, options: Intl.DateTimeFormatOptions = {}) {
|
||||
return new Date().toLocaleString("en-US", {
|
||||
timeZone,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ const schemaBookingBaseBodyParams = Booking.pick({
|
||||
|
||||
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
|
||||
|
||||
export const schemaBookingGetParams = z.object({
|
||||
dateFrom: iso8601.optional(),
|
||||
dateTo: iso8601.optional(),
|
||||
});
|
||||
|
||||
const schemaBookingEditParams = z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
@@ -28,7 +33,9 @@ const schemaBookingEditParams = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams.merge(schemaBookingEditParams);
|
||||
export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
|
||||
.merge(schemaBookingEditParams)
|
||||
.omit({ uid: true });
|
||||
|
||||
export const schemaBookingReadPublic = Booking.extend({
|
||||
attendees: z
|
||||
|
||||
@@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
})
|
||||
.merge(
|
||||
@@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
|
||||
@@ -5,14 +5,23 @@ import { _TeamModel as Team } from "@calcom/prisma/zod";
|
||||
export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true }).partial({
|
||||
hideBranding: true,
|
||||
metadata: true,
|
||||
pendingPayment: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({});
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
name: z.string().max(255),
|
||||
});
|
||||
|
||||
export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict();
|
||||
|
||||
export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial();
|
||||
|
||||
const schemaOwnerId = z.object({
|
||||
ownerId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const schemaTeamCreateBodyParams = schemaTeamBodyParams.merge(schemaOwnerId).strict();
|
||||
|
||||
export const schemaTeamReadPublic = Team.omit({});
|
||||
|
||||
export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic);
|
||||
|
||||
@@ -92,7 +92,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||
// for example making weekStart only accept weekdays as input
|
||||
const schemaUserEditParams = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
@@ -114,7 +114,7 @@ const schemaUserEditParams = z.object({
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
||||
+15
-2
@@ -1,6 +1,8 @@
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
module.exports = withAxiom({
|
||||
const plugins = [withAxiom];
|
||||
const nextConfig = {
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/core",
|
||||
@@ -66,4 +68,15 @@ module.exports = withAxiom({
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig["sentry"] = {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
};
|
||||
|
||||
plugins.push(withSentryConfig);
|
||||
}
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
@@ -6,18 +6,44 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, prisma, isAdmin, query } = req;
|
||||
if (isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { bookings: true },
|
||||
include: {
|
||||
bookings: true,
|
||||
teams: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
|
||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||
|
||||
if (!isAdmin && !userBookingIds.includes(id)) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
if (!userBookingIds.includes(id)) {
|
||||
const teamBookings = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
eventType: {
|
||||
team: {
|
||||
id: {
|
||||
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* type: boolean
|
||||
* description: Delete all remaining bookings
|
||||
* - in: query
|
||||
* name: reason
|
||||
* name: cancellationReason
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextApiRequest } from "next";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
@@ -119,13 +119,13 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
||||
*/
|
||||
function buildWhereClause(
|
||||
userId: number,
|
||||
userId: number | null,
|
||||
attendeeEmails: string[],
|
||||
userIds: number[] = [],
|
||||
userEmails: string[] = []
|
||||
) {
|
||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : { userId };
|
||||
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
||||
let whereClause = {};
|
||||
if (filterByAttendeeEmails) {
|
||||
whereClause = {
|
||||
@@ -162,6 +162,9 @@ function buildWhereClause(
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
|
||||
const { dateFrom, dateTo } = schemaBookingGetParams.parse(req.query);
|
||||
|
||||
const args: Prisma.BookingFindManyArgs = {};
|
||||
args.include = {
|
||||
attendees: true,
|
||||
@@ -189,7 +192,7 @@ async function handler(req: NextApiRequest) {
|
||||
const userEmails = users.map((u) => u.email);
|
||||
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
|
||||
} else if (filterByAttendeeEmails) {
|
||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||
args.where = buildWhereClause(null, attendeeEmails, [], []);
|
||||
}
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
@@ -203,6 +206,20 @@ async function handler(req: NextApiRequest) {
|
||||
}
|
||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
args.where = {
|
||||
...args.where,
|
||||
startTime: { gte: dateFrom },
|
||||
};
|
||||
}
|
||||
if (dateTo) {
|
||||
args.where = {
|
||||
...args.where,
|
||||
endTime: { lte: dateTo },
|
||||
};
|
||||
}
|
||||
|
||||
const data = await prisma.booking.findMany(args);
|
||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||
@@ -316,7 +318,13 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (!isAdmin && body.teamId)
|
||||
if (
|
||||
body.teamId &&
|
||||
!isAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.prisma, req.userId, isAdmin, body.teamId, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
}))
|
||||
)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `teamId`",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
@@ -9,10 +13,34 @@ import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
// Apply plugins
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const input = getScheduleSchema.parse(req.query);
|
||||
return await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||
const { usernameList, ...rest } = req.query;
|
||||
let slugs = usernameList;
|
||||
if (!Array.isArray(usernameList)) {
|
||||
slugs = usernameList ? [usernameList] : [];
|
||||
}
|
||||
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
|
||||
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||
const slotsInProvidedTimeZone = timeZoneSupported
|
||||
? Object.keys(availableSlots.slots).reduce(
|
||||
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||
acc[date] = availableSlots.slots[date].map((slot) => ({
|
||||
...slot,
|
||||
time: dayjs(slot.time).tz(input.timeZone).format(),
|
||||
}));
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: availableSlots;
|
||||
|
||||
return slotsInProvidedTimeZone;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
|
||||
@@ -27,6 +27,16 @@ export async function checkPermissions(
|
||||
version: req.query.version,
|
||||
apiKey: req.query.apiKey,
|
||||
});
|
||||
return canUserAccessTeamWithRole(prisma, userId, isAdmin, teamId, role);
|
||||
}
|
||||
|
||||
export async function canUserAccessTeamWithRole(
|
||||
prisma: NextApiRequest["prisma"],
|
||||
userId: number,
|
||||
isAdmin: boolean,
|
||||
teamId: number,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
|
||||
@@ -58,12 +58,44 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, body, userId } = req;
|
||||
const data = schemaTeamUpdateBodyParams.parse(body);
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
|
||||
/** Only OWNERS and ADMINS can edit teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
include: { members: true },
|
||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
||||
|
||||
const slugAlreadyExists = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: {
|
||||
mode: "insensitive",
|
||||
equals: data.slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (slugAlreadyExists && data.slug !== _team.slug)
|
||||
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||
|
||||
// Check if parentId is related to this user
|
||||
if (data.parentId && data.parentId === teamId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request: Parent id cannot be the same as the team id.",
|
||||
});
|
||||
}
|
||||
if (data.parentId) {
|
||||
const parentTeam = await prisma.team.findFirst({
|
||||
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!parentTeam)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||
});
|
||||
}
|
||||
|
||||
let paymentUrl;
|
||||
if (_team.slug === null && data.slug) {
|
||||
data.metadata = {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaMembershipPublic } from "~/lib/validations/membership";
|
||||
import { schemaTeamBodyParams, schemaTeamReadPublic } from "~/lib/validations/team";
|
||||
import { schemaTeamCreateBodyParams, schemaTeamReadPublic } from "~/lib/validations/team";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -31,6 +33,12 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "~/lib/validations/te
|
||||
* required:
|
||||
* - name
|
||||
* - slug
|
||||
* - hideBookATeamMember
|
||||
* - brandColor
|
||||
* - darkBrandColor
|
||||
* - timeZone
|
||||
* - weekStart
|
||||
* - isPrivate
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
@@ -38,6 +46,27 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "~/lib/validations/te
|
||||
* slug:
|
||||
* type: string
|
||||
* description: A unique slug that works as path for the team public page
|
||||
* hideBookATeamMember:
|
||||
* type: boolean
|
||||
* description: Flag to hide or show the book a team member option
|
||||
* brandColor:
|
||||
* type: string
|
||||
* description: Primary brand color for the team
|
||||
* darkBrandColor:
|
||||
* type: string
|
||||
* description: Dark variant of the primary brand color for the team
|
||||
* timeZone:
|
||||
* type: string
|
||||
* description: Time zone of the team
|
||||
* weekStart:
|
||||
* type: string
|
||||
* description: Starting day of the week for the team
|
||||
* isPrivate:
|
||||
* type: boolean
|
||||
* description: Flag indicating if the team is private
|
||||
* ownerId:
|
||||
* type: number
|
||||
* description: ID of the team owner - only admins can set this.
|
||||
* tags:
|
||||
* - teams
|
||||
* responses:
|
||||
@@ -49,23 +78,35 @@ import { schemaTeamBodyParams, schemaTeamReadPublic } from "~/lib/validations/te
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { prisma, body, userId } = req;
|
||||
const data = schemaTeamBodyParams.parse(body);
|
||||
const { prisma, body, userId, isAdmin } = req;
|
||||
const { ownerId, ...data } = schemaTeamCreateBodyParams.parse(body);
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
const effectiveUserId = isAdmin && ownerId ? ownerId : userId;
|
||||
|
||||
if (data.slug) {
|
||||
const alreadyExist = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: data.slug,
|
||||
slug: {
|
||||
mode: "insensitive",
|
||||
equals: data.slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (alreadyExist) throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
// Setting slug in metadata, so it can be published later
|
||||
data.metadata = {
|
||||
requestedSlug: data.slug,
|
||||
};
|
||||
delete data.slug;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if parentId is related to this user
|
||||
if (data.parentId) {
|
||||
const parentTeam = await prisma.team.findFirst({
|
||||
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!parentTeam)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Perhaps there is a better fix for this?
|
||||
@@ -75,24 +116,102 @@ async function postHandler(req: NextApiRequest) {
|
||||
...data,
|
||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||
};
|
||||
const team = await prisma.team.create({
|
||||
|
||||
if (!IS_TEAM_BILLING_ENABLED) {
|
||||
const team = await prisma.team.create({
|
||||
data: {
|
||||
...cloneData,
|
||||
members: {
|
||||
create: { userId: effectiveUserId, role: MembershipRole.OWNER, accepted: true },
|
||||
},
|
||||
},
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
req.statusCode = 201;
|
||||
|
||||
return {
|
||||
team: schemaTeamReadPublic.parse(team),
|
||||
owner: schemaMembershipPublic.parse(team.members[0]),
|
||||
message: `Team created successfully. We also made user with ID=${ownerId} the owner of this team.`,
|
||||
};
|
||||
}
|
||||
|
||||
const pendingPaymentTeam = await prisma.team.create({
|
||||
data: {
|
||||
...cloneData,
|
||||
createdAt: new Date(),
|
||||
members: {
|
||||
// We're also creating the relation membership of team ownership in this call.
|
||||
create: { userId, role: MembershipRole.OWNER, accepted: true },
|
||||
},
|
||||
pendingPayment: true,
|
||||
},
|
||||
include: { members: true },
|
||||
});
|
||||
req.statusCode = 201;
|
||||
// We are also returning the new ownership relation as owner besides team.
|
||||
|
||||
const checkoutSession = await generateTeamCheckoutSession({
|
||||
pendingPaymentTeamId: pendingPaymentTeam.id,
|
||||
ownerId: effectiveUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
team: schemaTeamReadPublic.parse(team),
|
||||
owner: schemaMembershipPublic.parse(team.members[0]),
|
||||
message: "Team created successfully, we also made you the owner of this team",
|
||||
message:
|
||||
"Your team will be created once we receive your payment. Please complete the payment using the payment link.",
|
||||
paymentLink: checkoutSession.url,
|
||||
pendingTeam: {
|
||||
...schemaTeamReadPublic.parse(pendingPaymentTeam),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isAdmin } = req;
|
||||
const body = schemaTeamCreateBodyParams.parse(req.body);
|
||||
|
||||
/* Non-admin users can only create teams for themselves */
|
||||
if (!isAdmin && body.ownerId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `ownerId`",
|
||||
});
|
||||
}
|
||||
|
||||
const generateTeamCheckoutSession = async ({
|
||||
pendingPaymentTeamId,
|
||||
ownerId,
|
||||
}: {
|
||||
pendingPaymentTeamId: number;
|
||||
ownerId: number;
|
||||
}) => {
|
||||
const customer = await getStripeCustomerIdFromUserId(ownerId);
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${WEBAPP_URL}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
|
||||
line_items: [
|
||||
{
|
||||
/** We only need to set the base price and we can upsell it directly on Stripe's checkout */
|
||||
price: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID,
|
||||
/**Initially it will be just the team owner */
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
customer_update: {
|
||||
address: "auto",
|
||||
},
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
},
|
||||
metadata: {
|
||||
pendingPaymentTeamId,
|
||||
ownerId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session.url)
|
||||
throw new HttpError({
|
||||
statusCode: 500,
|
||||
message: "Failed generating a checkout session URL.",
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
|
||||
@@ -69,7 +69,12 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, query, userId, isAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const { eventTypeId, userId: bodyUserId, ...data } = schemaWebhookEditBodyParams.parse(req.body);
|
||||
const {
|
||||
eventTypeId,
|
||||
userId: bodyUserId,
|
||||
eventTriggers,
|
||||
...data
|
||||
} = schemaWebhookEditBodyParams.parse(req.body);
|
||||
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
||||
|
||||
if (eventTypeId) {
|
||||
@@ -87,6 +92,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
if (args.data.eventTriggers) {
|
||||
const eventTriggersSet = new Set(eventTriggers);
|
||||
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||
}
|
||||
|
||||
const result = await prisma.webhook.update(args);
|
||||
return { webhook: schemaWebhookReadPublic.parse(result) };
|
||||
}
|
||||
|
||||
@@ -66,7 +66,12 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
const { eventTypeId, userId: bodyUserId, ...body } = schemaWebhookCreateBodyParams.parse(req.body);
|
||||
const {
|
||||
eventTypeId,
|
||||
userId: bodyUserId,
|
||||
eventTriggers,
|
||||
...body
|
||||
} = schemaWebhookCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
||||
|
||||
// If no event type, we assume is for the current user. If admin we run more checks below...
|
||||
@@ -87,6 +92,11 @@ async function postHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
if (args.data.eventTriggers) {
|
||||
const eventTriggersSet = new Set(eventTriggers);
|
||||
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||
}
|
||||
|
||||
const data = await prisma.webhook.create(args);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
@@ -8,7 +8,9 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
storybook-static
|
||||
storybook-static/*
|
||||
!storybook-static/favicon.ico
|
||||
!storybook-static/sb-cover.jpg
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/atoms/**/*.stories.mdx",
|
||||
"../../../packages/features/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-rtl-direction",
|
||||
"storybook-react-i18next",
|
||||
"storybook-addon-next",
|
||||
"storybook-addon-next-router",
|
||||
/*{
|
||||
name: "storybook-addon-next",
|
||||
options: {
|
||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
||||
},
|
||||
},*/
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
buffer: false,
|
||||
console: false,
|
||||
constants: false,
|
||||
crypto: false,
|
||||
domain: false,
|
||||
events: false,
|
||||
http: false,
|
||||
https: false,
|
||||
os: false,
|
||||
path: false,
|
||||
punycode: false,
|
||||
process: false,
|
||||
querystring: false,
|
||||
stream: false,
|
||||
string_decoder: false,
|
||||
sys: false,
|
||||
timers: false,
|
||||
tty: false,
|
||||
url: false,
|
||||
util: false,
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
typescript: { reactDocgen: "react-docgen" },
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
import path, { dirname, join } from "path";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx", // legacy SB6 stories
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/ui/components/**/*.docs.mdx",
|
||||
"../../../packages/features/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/features/**/*.docs.mdx",
|
||||
"../../../packages/atoms/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/atoms/**/*.docs.mdx",
|
||||
],
|
||||
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
getAbsolutePath("storybook-addon-rtl-direction"),
|
||||
getAbsolutePath("storybook-react-i18next"),
|
||||
],
|
||||
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/nextjs") as "@storybook/nextjs",
|
||||
|
||||
options: {
|
||||
// builder: {
|
||||
// fsCache: true,
|
||||
// lazyCompilation: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
|
||||
staticDirs: ["../public"],
|
||||
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
buffer: false,
|
||||
console: false,
|
||||
constants: false,
|
||||
crypto: false,
|
||||
domain: false,
|
||||
events: false,
|
||||
http: false,
|
||||
https: false,
|
||||
os: false,
|
||||
path: false,
|
||||
punycode: false,
|
||||
process: false,
|
||||
querystring: false,
|
||||
stream: false,
|
||||
string_decoder: false,
|
||||
sys: false,
|
||||
timers: false,
|
||||
tty: false,
|
||||
url: false,
|
||||
util: false,
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module = config.module || {};
|
||||
config.module.rules = config.module.rules || [];
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
typescript: { reactDocgen: "react-docgen" },
|
||||
|
||||
docs: {
|
||||
autodocs: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { addDecorator } from "@storybook/react";
|
||||
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
pathname: "/",
|
||||
asPath: "/",
|
||||
query: {},
|
||||
push() {},
|
||||
Provider: AppRouterContext.Provider,
|
||||
},
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||
</I18nextProvider>
|
||||
));
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// adds tooltip context to all stories
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import type { Preview } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import type { EmbedThemeConfig } from "@calcom/embed-core/src/types";
|
||||
// adds trpc context to all stories (esp. booker)
|
||||
import { StorybookTrpcProvider } from "@calcom/ui";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
|
||||
i18n,
|
||||
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StorybookTrpcProvider>
|
||||
<TooltipProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</TooltipProvider>
|
||||
</StorybookTrpcProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
getEmbedNamespace: () => string | null;
|
||||
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||
}
|
||||
}
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ArgsTable } from "@storybook/addon-docs";
|
||||
import { SortType } from "@storybook/components";
|
||||
import { PropDescriptor } from "@storybook/store";
|
||||
import type { SortType } from "@storybook/blocks";
|
||||
import type { PropDescriptor } from "@storybook/preview-api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
||||
type Component = any;
|
||||
|
||||
@@ -9,10 +9,7 @@ import { Meta } from "@storybook/addon-docs";
|
||||
library, we will be adding more components as we go along.
|
||||
</p>
|
||||
<p>
|
||||
Our{" "}
|
||||
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
|
||||
Figma
|
||||
</a>{" "}
|
||||
Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a>
|
||||
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
|
||||
the design team.
|
||||
</p>
|
||||
|
||||
+16
-15
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "start-storybook -p 6006",
|
||||
"build": "build-storybook"
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
@@ -20,23 +20,25 @@
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@storybook/addon-docs": "^7.6.3",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/nextjs": "^7.6.3",
|
||||
"@storybook/preview-api": "^7.6.3",
|
||||
"next": "^13.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"storybook-addon-rtl-direction": "^0.0.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/builder-vite": "^0.2.4",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-designs": "^7.0.7",
|
||||
"@storybook/addon-essentials": "^7.6.3",
|
||||
"@storybook/addon-interactions": "^7.6.3",
|
||||
"@storybook/addon-links": "^7.6.3",
|
||||
"@storybook/nextjs": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
@@ -46,9 +48,8 @@
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-pseudo-companion-classes": "^0.1.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-react-i18next": "^2.0.9",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
|
||||
@@ -197,88 +197,91 @@
|
||||
@layer {
|
||||
:root {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #e5e7eb;
|
||||
--cal-bg: white;
|
||||
--cal-bg-subtle: #f3f4f6;
|
||||
--cal-bg-muted: #f9fafb;
|
||||
--cal-bg-inverted: #111827;
|
||||
|
||||
|
||||
--cal-bg-emphasis: hsla(220,13%,91%,1);
|
||||
--cal-bg: hsla(0,0%,100%,1);
|
||||
--cal-bg-subtle: hsla(220, 14%, 96%,1);
|
||||
--cal-bg-muted: hsla(210,20%,98%,1);
|
||||
--cal-bg-inverted: hsla(0,0%,6%,1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
--cal-bg-info: hsla(218,83%,98%,1);
|
||||
--cal-bg-success: hsla(134,76%,94%,1);
|
||||
--cal-bg-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-bg-error: hsla(3,66,93,1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #9ca3af;
|
||||
--cal-border: #d1d5db;
|
||||
--cal-border-subtle: #e5e7eb;
|
||||
--cal-border-muted: #f3f4f6;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
--cal-border-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-border: hsla(216, 12%, 84%, 1);
|
||||
--cal-border-subtle: hsla(220, 13%, 91%, 1);
|
||||
--cal-border-booker: #e5e7eb;
|
||||
--cal-border-muted: hsla(220, 14%, 96%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #111827;
|
||||
--cal-text: #374151;
|
||||
--cal-text-subtle: #6b7280;
|
||||
--cal-text-muted: #9ca3af;
|
||||
--cal-text-inverted: white;
|
||||
|
||||
--cal-text-emphasis: hsla(217, 19%, 27%, 1);
|
||||
--cal-text: hsla(217, 19%, 27%, 1);
|
||||
--cal-text-subtle: hsla(220, 9%, 46%, 1);
|
||||
--cal-text-muted: hsla(218, 11%, 65%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
|
||||
--cal-text-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-text-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-text-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-text-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Brand shinanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand-text: white;
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: hsla(221, 39%, 11%, 1);
|
||||
--cal-brand-emphasis: hsla(0, 0%, 6%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 100%, 1);
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
--cal-bg: #101010;
|
||||
--cal-bg-subtle: #2b2b2b;
|
||||
--cal-bg-muted: #1c1c1c;
|
||||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
|
||||
--cal-bg-emphasis: hsla(0, 0%, 32%, 1);
|
||||
--cal-bg: hsla(0, 0%, 10%, 1);
|
||||
--cal-bg-subtle: hsla(0, 0%, 18%, 1);
|
||||
--cal-bg-muted: hsla(0, 0%, 12%, 1);
|
||||
--cal-bg-inverted: hsla(220, 14%, 96%, 1);
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
--cal-bg-info: hsla(228, 56%, 33%, 1);
|
||||
--cal-bg-success: hsla(133, 34%, 24%, 1);
|
||||
--cal-bg-attention: hsla(16, 62%, 28%, 1);
|
||||
--cal-bg-error: hsla(2, 55%, 30%, 1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #575757;
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2b2b2b;
|
||||
--cal-border-muted: #1c1c1c;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
--cal-border-emphasis: hsla(0, 0%, 46%, 1);
|
||||
--cal-border: hsla(0, 0%, 34%, 1);
|
||||
--cal-border-subtle: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-booker: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-muted: hsla(0, 0%, 18%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #f3f4f6;
|
||||
--cal-text: #d6d6d6;
|
||||
--cal-text-subtle: #757575;
|
||||
--cal-text-muted: #575757;
|
||||
--cal-text-inverted: #101010;
|
||||
|
||||
--cal-text-emphasis: hsla(240, 20%, 99%, 1);
|
||||
--cal-text: hsla(0, 0%, 84%, 1);
|
||||
--cal-text-subtle: hsla(0, 0%, 65%, 1);
|
||||
--cal-text-muted: hsla(0, 0%, 34%, 1);
|
||||
--cal-text-inverted: hsla(0, 0%, 10%, 1);
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
|
||||
--cal-text-info: hsla(218, 83%, 93%, 1);
|
||||
--cal-text-success: hsla(134, 76%, 94%, 1);
|
||||
--cal-text-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-text-error: hsla(3, 66%, 93%, 1);
|
||||
|
||||
/* Brand shenanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: white;
|
||||
--cal-brand-emphasis: #e1e1e1;
|
||||
--cal-brand-text: black;
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: hsla(0, 0%, 100%, 1);
|
||||
--cal-brand-emphasis: hsla(218, 11%, 65%, 1);
|
||||
--cal-brand-text: hsla(0, 0%, 0%,1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,19 @@ import z from "zod";
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
|
||||
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
|
||||
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
|
||||
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
|
||||
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
||||
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
|
||||
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
@@ -27,7 +40,6 @@ export const abTestMiddlewareFactory =
|
||||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||
|
||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||
|
||||
const enabled = route !== null ? route[1] || override : false;
|
||||
|
||||
if (pathname.includes("future") || !enabled) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
export const withAppDir =
|
||||
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
|
||||
async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
|
||||
if ("redirect" in ssrResponse) {
|
||||
redirect(ssrResponse.redirect.destination);
|
||||
}
|
||||
if ("notFound" in ssrResponse) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
...ssrResponse.props,
|
||||
// includes dehydratedState required for future page trpcPropvider
|
||||
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { isNotFoundError } from "next/dist/client/components/not-found";
|
||||
import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
export type EmbedProps = {
|
||||
isEmbed?: boolean;
|
||||
};
|
||||
|
||||
export default function withEmbedSsrAppDir<T extends Record<string, any>>(
|
||||
getData: (context: GetServerSidePropsContext) => Promise<T>
|
||||
) {
|
||||
return async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const { embed, layout } = context.query;
|
||||
|
||||
try {
|
||||
const props = await getData(context);
|
||||
|
||||
return {
|
||||
...props,
|
||||
isEmbed: true,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isRedirectError(e)) {
|
||||
const destinationUrl = getURLFromRedirectError(e);
|
||||
let urlPrefix = "";
|
||||
|
||||
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
|
||||
const destinationUrlObj = new URL(destinationUrl, WEBAPP_URL);
|
||||
|
||||
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
|
||||
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
|
||||
urlPrefix = destinationUrlObj.origin;
|
||||
} else {
|
||||
// Don't use any prefix for relative URLs to ensure we stay on the same domain
|
||||
urlPrefix = "";
|
||||
}
|
||||
|
||||
const destinationQueryStr = destinationUrlObj.searchParams.toString();
|
||||
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
|
||||
destinationQueryStr ? `${destinationQueryStr}&` : ""
|
||||
}layout=${layout}&embed=${embed}`;
|
||||
|
||||
redirect(newDestinationUrl);
|
||||
}
|
||||
|
||||
if (isNotFoundError(e)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { TRPCContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);
|
||||
@@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||
|
||||
const ENDPOINTS = [
|
||||
"admin",
|
||||
"apiKeys",
|
||||
"appRoutingForms",
|
||||
"apps",
|
||||
"auth",
|
||||
"availability",
|
||||
"appBasecamp3",
|
||||
"bookings",
|
||||
"deploymentSetup",
|
||||
"eventTypes",
|
||||
"features",
|
||||
"insights",
|
||||
"payments",
|
||||
"public",
|
||||
"saml",
|
||||
"slots",
|
||||
"teams",
|
||||
"organizations",
|
||||
"users",
|
||||
"viewer",
|
||||
"webhook",
|
||||
"workflows",
|
||||
"appsRouter",
|
||||
"googleWorkspace",
|
||||
] as const;
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export type Params = {
|
||||
[param: string]: string | string[] | undefined;
|
||||
};
|
||||
|
||||
export type SearchParams = {
|
||||
[param: string]: string | string[] | undefined;
|
||||
};
|
||||
|
||||
export type PageProps = {
|
||||
params: Params;
|
||||
searchParams: SearchParams;
|
||||
};
|
||||
|
||||
export type LayoutProps = { params: Params; children: React.ReactElement };
|
||||
@@ -1,22 +0,0 @@
|
||||
// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder
|
||||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type WrapperWithLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null })<"L">;
|
||||
@@ -0,0 +1,126 @@
|
||||
import AppPage from "@pages/apps/[slug]/index";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { notFound } from "next/navigation";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
data: z.object({
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
iframe: z.object({ src: z.string() }),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const { data } = await getPageProps({ params });
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${data.name} | ${APP_NAME}`,
|
||||
() => data.description
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
try {
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
if (typeof params?.slug !== "string") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const appMeta = await getAppWithMetadata({
|
||||
slug: params?.slug,
|
||||
});
|
||||
|
||||
const appFromDb = await prisma.app.findUnique({
|
||||
where: { slug: params.slug.toLowerCase() },
|
||||
});
|
||||
|
||||
const isAppAvailableInFileSystem = appMeta;
|
||||
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||
|
||||
if (!IS_PRODUCTION && isAppDisabled) {
|
||||
return {
|
||||
isAppDisabled: true as const,
|
||||
data: {
|
||||
...appMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isTemplate = appMeta.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
try {
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||
} catch (error) {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return getAppAssetFullPath(item, {
|
||||
dirName: appMeta.dirName,
|
||||
isTemplate: appMeta.isTemplate,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return {
|
||||
isAppDisabled: false as const,
|
||||
source: { content, data },
|
||||
data: appMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
|
||||
return <AppPage {...pageProps} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@@ -0,0 +1,36 @@
|
||||
import SetupPage from "@pages/apps/[slug]/setup";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
return await _generateMetadata(
|
||||
() => `${params.slug} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||
|
||||
if (!result || "notFound" in result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if ("redirect" in result) {
|
||||
redirect(result.redirect.destination);
|
||||
}
|
||||
|
||||
return result.props;
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
return <SetupPage {...pageProps} />;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import CategoryPage from "@pages/apps/categories/[category]";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `${APP_NAME} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const appQuery = await prisma.app.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
has: p.data.category,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||
|
||||
const appStore = await getAppRegistry();
|
||||
|
||||
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||
return {
|
||||
apps,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error getData arg
|
||||
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<"P">;
|
||||
export const dynamic = "force-static";
|
||||
@@ -0,0 +1,44 @@
|
||||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Categories | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
}
|
||||
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null })<"L">;
|
||||
@@ -0,0 +1,36 @@
|
||||
import LegacyPage from "@pages/apps/installed/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("installed_apps")} | ${APP_NAME}`,
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
category: p.data.category,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
await getPageProps({ params });
|
||||
|
||||
return <LegacyPage />;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import AppsPage from "@pages/apps";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Apps | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
let appStore, userAdminTeams: UserAdminTeams;
|
||||
if (session?.user?.id) {
|
||||
userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
userAdminTeams = [];
|
||||
}
|
||||
|
||||
const categoryQuery = appStore.map(({ categories }) => ({
|
||||
categories: categories || [],
|
||||
}));
|
||||
|
||||
const categories = categoryQuery.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories)
|
||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||
name: name as AppCategories,
|
||||
count,
|
||||
}))
|
||||
.sort(function (a, b) {
|
||||
return b.count - a.count;
|
||||
}),
|
||||
appStore,
|
||||
userAdminTeams,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page: AppsPage });
|
||||
@@ -0,0 +1,10 @@
|
||||
import OldPage from "@pages/booking/[uid]";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getData } from "../page";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
// @ts-expect-error Type '(context: GetServerSidePropsContext) => Promise<any>' is not assignable to type '(arg: {
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
|
||||
@@ -0,0 +1,204 @@
|
||||
import OldPage from "@pages/booking/[uid]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } from "@lib/booking";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const stringToBoolean = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
email: z.string().optional(),
|
||||
eventTypeSlug: z.string().optional(),
|
||||
cancel: stringToBoolean,
|
||||
allRemainingBookings: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "",
|
||||
() => ""
|
||||
);
|
||||
|
||||
export const getData = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const session = await getServerSession(context);
|
||||
let tz: string | null = null;
|
||||
let userTimeFormat: number | null = null;
|
||||
let requiresLoginToUpdate = false;
|
||||
if (session) {
|
||||
const user = await ssr.viewer.me.fetch();
|
||||
tz = user.timeZone;
|
||||
userTimeFormat = user.timeFormat;
|
||||
}
|
||||
|
||||
const parsedQuery = querySchema.safeParse(context.query);
|
||||
|
||||
if (!parsedQuery.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||
|
||||
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: maybeUid,
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
smsReminderNumber: true,
|
||||
recurringEventId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
select: {
|
||||
referenceUid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!bookingInfoRaw) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||
? getDefaultEvent(eventTypeSlug || "")
|
||||
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||
if (!eventTypeRaw) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||
requiresLoginToUpdate = true;
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||
|
||||
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
||||
? eventTypeRaw.hosts.map((host) => host.user)
|
||||
: eventTypeRaw.users;
|
||||
|
||||
if (!eventTypeRaw.users.length) {
|
||||
if (!eventTypeRaw.owner) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
eventTypeRaw.users.push({
|
||||
...eventTypeRaw.owner,
|
||||
});
|
||||
}
|
||||
|
||||
const eventType = {
|
||||
...eventTypeRaw,
|
||||
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||
};
|
||||
|
||||
const profile = {
|
||||
name: eventType.team?.name || eventType.users[0]?.name || null,
|
||||
email: eventType.team ? null : eventType.users[0].email || null,
|
||||
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
||||
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
||||
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||
};
|
||||
|
||||
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
|
||||
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
|
||||
}
|
||||
|
||||
const payment = await prisma.payment.findFirst({
|
||||
where: {
|
||||
bookingId: bookingInfo.id,
|
||||
},
|
||||
select: {
|
||||
success: true,
|
||||
refunded: true,
|
||||
currency: true,
|
||||
amount: true,
|
||||
paymentOption: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||
profile,
|
||||
eventType,
|
||||
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||
bookingInfo,
|
||||
paymentStatus: payment,
|
||||
...(tz && { tz }),
|
||||
userTimeFormat,
|
||||
requiresLoginToUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error Argument of type '{ req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
|
||||
export default WithLayout({ getLayout: null, getData, Page: OldPage });
|
||||
@@ -0,0 +1,45 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||
|
||||
const querySchema = z.object({
|
||||
status: z.enum(validStatuses),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => `${APP_NAME} | ${t("bookings")}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const parsedParams = querySchema.safeParse(ctx.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const ssg = await ssgInit(ctx);
|
||||
|
||||
return {
|
||||
status: parsedParams.data.status,
|
||||
dehydratedState: ssg.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout, getData })<"L">;
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@pages/bookings/[status]";
|
||||
@@ -0,0 +1,4 @@
|
||||
import LegacyPage from "@pages/connect-and-join";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,132 @@
|
||||
import LegacyPage, { type PageProps } from "@pages/d/[link]/[slug]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, getMultipleDurationValue } 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 slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const pageProps = await getPageProps(
|
||||
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
|
||||
);
|
||||
|
||||
const { entity, booking, user, slug, isTeamEvent } = pageProps;
|
||||
const rescheduleUid = booking?.uid;
|
||||
const { trpc } = await import("@calcom/trpc");
|
||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||
{ username: user ?? "", eventSlug: slug ?? "", isTeamEvent, org: entity.orgSlug ?? null },
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const title = event?.title ?? "";
|
||||
return await _generateMetadata(
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
|
||||
);
|
||||
};
|
||||
|
||||
async function getPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req: context.req });
|
||||
const { link, slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const hashedLink = await prisma.hashedLink.findUnique({
|
||||
where: {
|
||||
link,
|
||||
},
|
||||
select: {
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const username = hashedLink?.eventType.users[0]?.username;
|
||||
|
||||
if (!hashedLink || !username) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
}
|
||||
|
||||
const isTeamEvent = !!hashedLink.eventType?.team?.id;
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
// 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, isTeamEvent, org });
|
||||
|
||||
if (!eventData) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(eventData.metadata?.multipleDuration, queryDuration, eventData.length),
|
||||
booking,
|
||||
away: user?.away,
|
||||
user: username,
|
||||
slug,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
// Sending the team event from the server, because this template file
|
||||
// is reused for both team and user events.
|
||||
isTeamEvent,
|
||||
hashedLink: link,
|
||||
};
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
|
||||
|
||||
// @ts-expect-error arg
|
||||
const getData = withAppDir<PageProps>(getPageProps);
|
||||
export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import EnterprisePage from "@components/EnterprisePage";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("create_your_org"),
|
||||
(t) => t("create_your_org_description")
|
||||
);
|
||||
|
||||
export default EnterprisePage;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -0,0 +1,57 @@
|
||||
import LegacyPage from "@pages/getting-started/[[...step]]";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
const ssr = await ssrInit(ctx);
|
||||
await ssr.viewer.me.prefetch();
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
accepted: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User from session not found");
|
||||
}
|
||||
|
||||
if (user.completedOnboarding) {
|
||||
redirect("/event-types");
|
||||
}
|
||||
|
||||
return {
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
||||
requiresLicense: false,
|
||||
themeBasis: null,
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
|
||||
@@ -0,0 +1,26 @@
|
||||
import LegacyPage from "@pages/insights/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Insights",
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
async function getData() {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
|
||||
if (flags.insights === false) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page: LegacyPage });
|
||||
@@ -0,0 +1,13 @@
|
||||
import LegacyPage from "@pages/maintenance";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => `${t("under_maintenance")} | ${APP_NAME}`,
|
||||
(t) => t("under_maintenance_description", { appName: APP_NAME })
|
||||
);
|
||||
|
||||
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Page from "@pages/more";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null, Page })<"P">;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
@@ -0,0 +1,9 @@
|
||||
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
const getData = withAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import Page from "@pages/org/[orgSlug]/[user]/embed";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
const getData = withAppDir<PageProps>(getServerSideProps);
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
|
||||
@@ -0,0 +1,7 @@
|
||||
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
const getData = withAppDir<PageProps>(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
|
||||
@@ -0,0 +1,167 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
|
||||
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
// the title does not contain the eventName as in the legacy page
|
||||
(t) => `${t("payment")} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
});
|
||||
|
||||
async function getData(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
await ssr.viewer.me.prefetch();
|
||||
|
||||
const { uid } = querySchema.parse(context.params);
|
||||
const rawPayment = await prisma.payment.findFirst({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
data: true,
|
||||
success: true,
|
||||
uid: true,
|
||||
refunded: true,
|
||||
bookingId: true,
|
||||
appId: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
paymentOption: true,
|
||||
booking: {
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
location: true,
|
||||
status: true,
|
||||
rejectionReason: true,
|
||||
cancellationReason: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
eventName: true,
|
||||
requiresConfirmation: true,
|
||||
userId: true,
|
||||
metadata: true,
|
||||
users: {
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
price: true,
|
||||
currency: true,
|
||||
successRedirectUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rawPayment) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { data, booking: _booking, ...restPayment } = rawPayment;
|
||||
|
||||
const payment = {
|
||||
...restPayment,
|
||||
data: data as Record<string, unknown>,
|
||||
};
|
||||
|
||||
if (!_booking) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { startTime, endTime, eventType, ...restBooking } = _booking;
|
||||
const booking = {
|
||||
...restBooking,
|
||||
startTime: startTime.toString(),
|
||||
endTime: endTime.toString(),
|
||||
};
|
||||
|
||||
if (!eventType) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (eventType.users.length === 0 && !!!eventType.team) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [user] = eventType?.users.length
|
||||
? eventType.users
|
||||
: [{ name: null, theme: null, hideBranding: null, username: null }];
|
||||
const profile = {
|
||||
name: eventType.team?.name || user?.name || null,
|
||||
theme: (!eventType.team?.name && user?.theme) || null,
|
||||
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
|
||||
};
|
||||
|
||||
if (
|
||||
([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes(
|
||||
booking.status as BookingStatus
|
||||
)
|
||||
) {
|
||||
return redirect(`/booking/${booking.uid}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
eventType: {
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
},
|
||||
booking,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
payment,
|
||||
clientSecret: getClientSecretFromPayment(payment),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: PaymentPage });
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getServerSideProps } from "@pages/reschedule/[uid]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
type PageProps = Readonly<{
|
||||
params: Params;
|
||||
}>;
|
||||
|
||||
const Page = async ({ params }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
|
||||
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
|
||||
await withAppDir(withEmbedSsr(getServerSideProps))(legacyCtx);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,30 @@
|
||||
import OldPage, { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]";
|
||||
import { withAppDir } from "app/AppDirSSRHOC";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "",
|
||||
() => ""
|
||||
);
|
||||
|
||||
type PageProps = Readonly<{
|
||||
params: Params;
|
||||
}>;
|
||||
|
||||
const getData = withAppDir(_getServerSideProps);
|
||||
|
||||
const Page = async ({ params }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
|
||||
|
||||
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
|
||||
await getData(legacyCtx);
|
||||
|
||||
return <OldPage />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,10 @@
|
||||
import Page from "@pages/settings/admin/apps/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("apps"),
|
||||
(t) => t("admin_apps_description")
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -0,0 +1,10 @@
|
||||
import Page from "@pages/settings/admin/apps/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("apps"),
|
||||
(t) => t("admin_apps_description")
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -0,0 +1,10 @@
|
||||
import Page from "@pages/settings/admin/flags";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Feature Flags",
|
||||
() => "Here you can toggle your Cal.com instance features."
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getLayout })<"L">;
|
||||
@@ -0,0 +1,10 @@
|
||||
import Page from "@pages/settings/admin/impersonation";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("admin"),
|
||||
(t) => t("impersonation")
|
||||
);
|
||||
|
||||
export default Page;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user