Merge branch 'main' into rkreddy99/cal-2330

This commit is contained in:
Keith Williams 2024-01-10 11:12:22 -03:00 committed by GitHub
commit 031bece4c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
439 changed files with 11895 additions and 3294 deletions

View File

@ -127,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/
# *********************************************************************************************************

View File

@ -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=
@ -249,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
@ -260,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
@ -302,3 +321,13 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
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

View File

@ -19,12 +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)
- [ ] Tests (Unit/Integration/E2E or any other test)
- [ ] 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?

View File

@ -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

View File

@ -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

View File

@ -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),
});
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
@ -24,11 +24,10 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shard: [1, 2, 3, 4, 5]
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

View File

@ -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: "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"
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),
});
}

View File

@ -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

View File

@ -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,39 +55,39 @@ 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
- name: Get comment body
id: get-comment-body
if: success() && github.event.number
if: success()
run: |
cd apps/web
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
echo "{name}={$body}" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v1
if: success() && github.event.number
uses: peter-evans/find-comment@v2
if: success()
id: fc
with:
issue-number: ${{ github.event.number }}
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 }}

View File

@ -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"

View File

@ -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

82
.github/workflows/pre-release.yml vendored Normal file
View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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"],

View File

@ -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,7 +216,7 @@ 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`.
@ -554,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

View File

@ -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

View File

@ -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!

View File

@ -64,6 +64,25 @@ export async function patchHandler(req: NextApiRequest) {
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
// 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 = {

View File

@ -68,6 +68,18 @@ async function postHandler(req: NextApiRequest) {
}
}
// 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?
const cloneData: typeof data & {
metadata: NonNullable<typeof data.metadata> | undefined;

View File

@ -1,212 +0,0 @@
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts
// repo link: https://github.com/trpc/next-13
// code is / will continue to be adapted for our usage
import { dehydrate, QueryClient } from "@tanstack/query-core";
import type { DehydratedState, QueryKey } from "@tanstack/react-query";
import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc";
import {
callProcedure,
type AnyProcedure,
type AnyQueryProcedure,
type AnyRouter,
type DataTransformer,
type inferProcedureInput,
type inferProcedureOutput,
type inferRouterContext,
type MaybePromise,
type ProcedureRouterRecord,
} from "@calcom/trpc/server";
import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared";
export function getArrayQueryKey(
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
type: string
): QueryKey {
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
const [arrayPath, input] = queryKeyArrayed;
if (!input && (!type || type === "any")) {
return Array.isArray(arrayPath) && arrayPath.length !== 0 ? [arrayPath] : ([] as unknown as QueryKey);
}
return [
arrayPath,
{
...(typeof input !== "undefined" && { input: input }),
...(type && type !== "any" && { type: type }),
},
];
}
// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58
function transformQueryOrMutationCacheErrors<
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0]
>(result: TState): TState {
const error = result.state.error as Maybe<TRPCClientError<any>>;
if (error instanceof Error && error.name === "TRPCClientError") {
const newError: TRPCClientErrorLike<any> = {
message: error.message,
data: error.data,
shape: error.shape,
};
return {
...result,
state: {
...result.state,
error: newError,
},
};
}
return result;
}
// copy ends
interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> {
router: TRouter;
createContext: () => MaybePromise<inferRouterContext<TRouter>>;
transformer?: DataTransformer;
}
/**
* @internal
*/
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure
? {
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
}
: never;
type OmitNever<TType> = Pick<
TType,
{
[K in keyof TType]: TType[K] extends never ? never : K;
}[keyof TType]
>;
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TPath extends string = ""
> = OmitNever<{
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`>
: TProcedures[TKey] extends AnyQueryProcedure
? DecorateProcedure<TProcedures[TKey]>
: never;
}>;
type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & {
dehydrate(): Promise<DehydratedState>;
queryClient: QueryClient;
};
const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => {
let _trpc: {
queryClient: QueryClient;
context: inferRouterContext<TRouter>;
} | null = null;
return () => {
if (_trpc === null) {
_trpc = {
context: opts.createContext(),
queryClient: new QueryClient(),
};
}
return _trpc;
};
};
export function createTRPCNextLayout<TRouter extends AnyRouter>(
opts: CreateTRPCNextLayoutOptions<TRouter>
): CreateTRPCNextLayout<TRouter> {
const getState = getStateContainer(opts);
const transformer = opts.transformer ?? {
serialize: (v) => v,
deserialize: (v) => v,
};
return createFlatProxy((key) => {
const state = getState();
const { queryClient } = state;
if (key === "queryClient") {
return queryClient;
}
if (key === "dehydrate") {
// copy starts
// copied from trpc/trpc repo
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229
const dehydratedCache = dehydrate(queryClient, {
shouldDehydrateQuery() {
// makes sure errors are also dehydrated
return true;
},
});
// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
const dehydratedCacheWithErrors = {
...dehydratedCache,
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors),
};
return () => transformer.serialize(dehydratedCacheWithErrors);
}
// copy ends
return createRecursiveProxy(async (callOpts) => {
const path = [key, ...callOpts.path];
const utilName = path.pop();
const ctx = await state.context;
const caller = opts.router.createCaller(ctx);
const pathStr = path.join(".");
const input = callOpts.args[0];
if (utilName === "fetchInfinite") {
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}
if (utilName === "prefetch") {
return queryClient.prefetchQuery({
queryKey: getArrayQueryKey([path, input], "query"),
queryFn: async () => {
const res = await callProcedure({
procedures: opts.router._def.procedures,
path: pathStr,
rawInput: input,
ctx,
type: "query",
});
return res;
},
});
}
if (utilName === "prefetchInfinite") {
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
caller.query(pathStr, input)
);
}
return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () =>
caller.query(pathStr, input)
);
}) as CreateTRPCNextLayout<TRouter>;
});
}

View File

@ -1,34 +0,0 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import superjson from "superjson";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";
import { createTRPCNextLayout } from "./createTRPCNextLayout";
export async function ssgInit() {
const locale = headers().get("x-locale") ?? "en";
const i18n = (await serverSideTranslations(locale, ["common"])) || "en";
const ssg = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return { prisma, insightsDb: readonlyPrisma, session: null, locale, i18n };
},
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
ssg.queryClient.setQueryData(queryKey, { i18n });
return ssg;
}

View File

@ -1,57 +0,0 @@
import { type GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers, cookies } from "next/headers";
import superjson from "superjson";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";
import { createTRPCNextLayout } from "./createTRPCNextLayout";
export async function ssrInit(options?: { noI18nPreload: boolean }) {
const req = {
headers: headers(),
cookies: cookies(),
};
const locale = await getLocale(req);
const i18n = (await serverSideTranslations(locale, ["common", "vital"])) || "en";
const ssr = createTRPCNextLayout({
router: appRouter,
transformer: superjson,
createContext() {
return {
prisma,
insightsDb: readonlyPrisma,
session: null,
locale,
i18n,
req: req as unknown as GetServerSidePropsContext["req"],
};
},
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
if (!options?.noI18nPreload) {
ssr.queryClient.setQueryData(queryKey, { i18n });
}
await Promise.allSettled([
// So feature flags are available on first render
ssr.viewer.features.map.prefetch(),
// Provides a better UX to the users who have already upgraded.
ssr.viewer.teams.hasTeamPlan.prefetch(),
ssr.viewer.public.session.prefetch(),
]);
return ssr;
}

View File

@ -1,3 +0,0 @@
export type Params = {
[param: string]: string | string[] | undefined;
};

View File

@ -1,15 +0,0 @@
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
type EventTypesLayoutProps = {
children: ReactElement;
};
export default function Layout({ children }: EventTypesLayoutProps) {
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -1,15 +0,0 @@
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
type EventTypesLayoutProps = {
children: ReactElement;
};
export default function Layout({ children }: EventTypesLayoutProps) {
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -1,20 +0,0 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
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>
);
}

View File

@ -1,10 +0,0 @@
import Page from "@pages/settings/admin/oAuth/index";
import { _generateMetadata } from "_app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "OAuth",
() => "Add new OAuth Clients"
);
export default Page;

View File

@ -1,10 +0,0 @@
import Page from "@pages/settings/admin/index";
import { _generateMetadata } from "_app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Admin",
() => "admin_description"
);
export default Page;

View File

@ -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>
);
}

View File

@ -1,20 +0,0 @@
// pages containing layout (e.g., /availability/[schedule].tsx) are supposed to go under (no-layout) folder
import { headers } from "next/headers";
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithoutLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithoutLayout({ children }: WrapperWithoutLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -1,21 +0,0 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
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>
);
}

View File

@ -11,6 +11,13 @@ const ROUTES: [URLPattern, boolean][] = [
["/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,

View File

@ -1,6 +1,6 @@
"use client";
import { createHydrateClient } from "_app/_trpc/createHydrateClient";
import { createHydrateClient } from "app/_trpc/createHydrateClient";
import superjson from "superjson";
export const HydrateClient = createHydrateClient({

View File

@ -1,6 +1,6 @@
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { HydrateClient } from "_app/_trpc/HydrateClient";
import { trpc } from "_app/_trpc/client";
import { HydrateClient } from "app/_trpc/HydrateClient";
import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";

14
apps/web/app/_types.ts Normal file
View File

@ -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 };

View File

@ -0,0 +1,3 @@
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null })<"L">;

View File

@ -1,6 +1,6 @@
import AppPage from "@pages/apps/[slug]/index";
import { Prisma } from "@prisma/client";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import fs from "fs";
import matter from "gray-matter";
import { notFound } from "next/navigation";

View File

@ -1,5 +1,5 @@
import SetupPage from "@pages/apps/[slug]/setup";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { notFound, redirect } from "next/navigation";

View File

@ -1,6 +1,7 @@
import CategoryPage from "@pages/apps/categories/[category]";
import { Prisma } from "@prisma/client";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import z from "zod";
@ -9,8 +10,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `${APP_NAME} | ${APP_NAME}`,
@ -67,13 +66,6 @@ const getPageProps = async ({ params }: { params: Record<string, string | string
};
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
const { apps } = await getPageProps({ params });
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
<CategoryPage apps={apps} />
</PageWrapper>
);
}
// @ts-expect-error getData arg
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<"P">;
export const dynamic = "force-static";

View File

@ -1,13 +1,14 @@
import LegacyPage from "@pages/apps/categories/index";
import { ssrInit } from "_app/_trpc/ssrInit";
import { _generateMetadata } from "_app/_utils";
import { cookies, headers } from "next/headers";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import PageWrapper from "@components/PageWrapperAppDir";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
return await _generateMetadata(
@ -16,12 +17,12 @@ export const generateMetadata = async () => {
);
};
async function getPageProps() {
const ssr = await ssrInit();
const req = { headers: headers(), cookies: cookies() };
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
const session = await getServerSession({ req });
const session = await getServerSession({ req: ctx.req });
let appStore;
if (session?.user?.id) {
@ -39,18 +40,8 @@ async function getPageProps() {
return {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
dehydratedState: await ssr.dehydrate(),
dehydratedState: ssr.dehydrate(),
};
}
};
export default async function Page() {
const props = await getPageProps();
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<LegacyPage {...props} />
</PageWrapper>
);
}
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;

View File

@ -0,0 +1,3 @@
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null })<"L">;

View File

@ -1,5 +1,5 @@
import LegacyPage from "@pages/apps/installed/[category]";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation";
import { z } from "zod";
@ -30,7 +30,7 @@ const getPageProps = async ({ params }: { params: Record<string, string | string
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
const { category } = await getPageProps({ params });
await getPageProps({ params });
return <LegacyPage />;
}

View File

@ -0,0 +1,66 @@
import AppsPage from "@pages/apps";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
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 type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `Apps | ${APP_NAME}`,
() => ""
);
};
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
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 });

View File

@ -0,0 +1,46 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
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: ReturnType<typeof buildLegacyCtx>) => {
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";

View File

@ -0,0 +1 @@
export { default } from "@pages/bookings/[status]";

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,5 +1,5 @@
import EventTypes from "@pages/event-types";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,63 @@
import LegacyPage from "@pages/getting-started/[[...step]]";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
const req = { headers: headers(), cookies: cookies() };
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
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: any) => team.accepted === false) ?? false,
requiresLicense: false,
themeBasis: null,
};
};
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });

View File

@ -1,5 +1,5 @@
import Page from "@pages/settings/admin/apps/[category]";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,5 +1,5 @@
import Page from "@pages/settings/admin/apps/index";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,5 +1,5 @@
import Page from "@pages/settings/admin/flags";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,5 +1,5 @@
import Page from "@pages/settings/admin/impersonation";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,3 @@
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null })<"L">;

View File

@ -1,5 +1,5 @@
import Page from "@pages/settings/admin/oAuth/oAuthView";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(

View File

@ -0,0 +1,13 @@
import LegacyPage from "@pages/settings/admin/oAuth/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
() => "OAuth",
() => "Add new OAuth Clients"
);
export default WithLayout({ getLayout, Page: LegacyPage })<"P">;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,4 +1,4 @@
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";

View File

@ -0,0 +1,13 @@
import LegacyPage from "@pages/settings/admin/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Admin",
() => "admin_description"
);
export default WithLayout({ getLayout, Page: LegacyPage })<"P">;

View File

@ -1,6 +1,6 @@
import { getServerCaller } from "_app/_trpc/serverClient";
import { type Params } from "_app/_types";
import { _generateMetadata } from "_app/_utils";
import { getServerCaller } from "app/_trpc/serverClient";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { z } from "zod";

View File

@ -1,4 +1,4 @@
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/users/pages/users-add-view";

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -1,4 +1,4 @@
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/users/pages/users-listing-view";

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("booking_appearance"),
(t) => t("appearance_team_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/billing/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("team_billing_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("team_members"),
(t) => t("members_team_description")
);
export default Page;

View File

@ -0,0 +1,11 @@
import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/onboard-members";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("add_team_members"),
(t) => t("add_team_members_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-profile-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("profile"),
(t) => t("profile_team_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/sso/page/teams-sso-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sso_configuration"),
(t) => t("sso_configuration_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import LegacyPage, { LayoutWrapper } from "@pages/settings/teams/new/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("create_new_team"),
(t) => t("create_new_team_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper })<"P">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
export default Page;

View File

@ -0,0 +1,40 @@
import OldPage from "@pages/teams/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { redirect } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
req: context.req,
});
if (!session) {
const token = Array.isArray(context.query.token) ? context.query.token[0] : context.query.token;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
return redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
}
return { dehydratedState: ssr.dehydrate() };
}
export default WithLayout({ getData, getLayout, Page: OldPage })<"P">;

View File

@ -0,0 +1,105 @@
import OldPage from "@pages/video/[uid]";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MarkdownIt from "markdown-it";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
() => `${APP_NAME} Video`,
(t) => t("quick_video_meeting")
);
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({
where: {
uid: context.query.uid as string,
},
select: {
...bookingMinimalSelect,
uid: true,
description: true,
isRecorded: true,
user: {
select: {
id: true,
timeZone: true,
name: true,
email: true,
organization: {
select: {
calVideoLogo: true,
},
},
},
},
references: {
select: {
uid: true,
type: true,
meetingUrl: true,
meetingPassword: true,
},
where: {
type: "daily_video",
},
},
},
});
if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) {
return redirect("/video/no-meeting-found");
}
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
const now = new Date();
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
//find out if the meeting is in the past
const isPast = booking?.endTime <= exitDate;
if (isPast) {
return redirect(`/video/meeting-ended/${booking?.uid}`);
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
const session = await getServerSession({ req: context.req });
// set meetingPassword to null for guests
if (session?.user.id !== bookingObj.user?.id) {
bookingObj.references.forEach((bookRef: any) => {
bookRef.meetingPassword = null;
});
}
return {
meetingUrl: bookingObj.references[0].meetingUrl ?? "",
...(typeof bookingObj.references[0].meetingPassword === "string" && {
meetingPassword: bookingObj.references[0].meetingPassword,
}),
booking: {
...bookingObj,
...(bookingObj.description && { description: md.render(bookingObj.description) }),
},
dehydratedState: ssr.dehydrate(),
};
}
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -0,0 +1,53 @@
import OldPage from "@pages/video/meeting-ended/[uid]";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Meeting Unavailable",
() => "Meeting Unavailable"
);
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const booking = await prisma.booking.findUnique({
where: {
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
},
select: {
...bookingMinimalSelect,
uid: true,
user: {
select: {
credentials: true,
},
},
references: {
select: {
uid: true,
type: true,
meetingUrl: true,
},
},
},
});
if (!booking) {
return redirect("/video/no-meeting-found");
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
return {
booking: bookingObj,
};
}
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -0,0 +1,51 @@
import OldPage from "@pages/video/meeting-not-started/[uid]";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
type PageProps = Readonly<{
params: Params;
}>;
export const generateMetadata = async ({ params }: PageProps) => {
const booking = await prisma.booking.findUnique({
where: {
uid: typeof params?.uid === "string" ? params.uid : "",
},
select: bookingMinimalSelect,
});
return await _generateMetadata(
(t) => t("this_meeting_has_not_started_yet"),
() => booking?.title ?? ""
);
};
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const booking = await prisma.booking.findUnique({
where: {
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
},
select: bookingMinimalSelect,
});
if (!booking) {
return redirect("/video/no-meeting-found");
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
return {
booking: bookingObj,
};
}
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -0,0 +1,24 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("no_meeting_found"),
(t) => t("no_meeting_found")
);
const getData = async (context: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
return {
dehydratedState: ssr.dehydrate(),
};
};
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;

View File

@ -84,7 +84,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
`}</style>
</head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
className="dark:bg-darkgray-50 todesktop:!bg-transparent bg-subtle antialiased"
style={
isEmbed
? {

View File

@ -0,0 +1,28 @@
import type { LayoutProps, PageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
type WithLayoutParams<T extends Record<string, any>> = {
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
Page?: (props: T) => React.ReactElement;
getData?: (arg: ReturnType<typeof buildLegacyCtx>) => Promise<T>;
};
export function WithLayout<T extends Record<string, any>>({ getLayout, getData, Page }: WithLayoutParams<T>) {
return async <P extends "P" | "L">(p: P extends "P" ? PageProps : LayoutProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const props = getData ? await getData(buildLegacyCtx(h, cookies(), p.params)) : ({} as T);
const children = "children" in p ? p.children : null;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
{Page ? <Page {...props} /> : children}
</PageWrapper>
);
};
}

View File

@ -20,7 +20,7 @@ export interface CalPageWrapper {
export type PageWrapperProps = Readonly<{
getLayout: ((page: React.ReactElement) => ReactNode) | null;
children: React.ReactElement;
children: React.ReactNode;
requiresLicense: boolean;
nonce: string | undefined;
themeBasis: string | null;
@ -62,7 +62,7 @@ function PageWrapper(props: PageWrapperProps) {
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
{getLayout(
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : <>{props.children}</>
)}
</>
</AppProviders>

Some files were not shown because too many files have changed in this diff Show More