Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14edd6c42f | ||
|
|
1c5f05638f | ||
|
|
fe4017f302 | ||
|
|
2abfae7f13 | ||
|
|
75d8300635 | ||
|
|
67f20f4503 | ||
|
|
d6376c6d70 | ||
|
|
9256d306b9 | ||
|
|
b62493235c | ||
|
|
f35b36fafa | ||
|
|
2689cdfdff | ||
|
|
efcf68869d | ||
|
|
8afe87ff75 | ||
|
|
7847177e8b | ||
|
|
866c8c046d | ||
|
|
96f8a89efa | ||
|
|
a2cb529d62 | ||
|
|
9c680838b6 | ||
|
|
6650fdb9ef | ||
|
|
9909ac53fc | ||
|
|
f598a8ebdd | ||
|
|
23920fe9ce | ||
|
|
33eee8dfc3 | ||
|
|
f5b9b6b0f9 | ||
|
|
b664857809 | ||
|
|
c746fdcdfe | ||
|
|
76b53eb3d4 | ||
|
|
090247a058 | ||
|
|
e23354b48b | ||
|
|
0b7f51410a | ||
|
|
31da9eaa8f | ||
|
|
192d927232 | ||
|
|
f9ff3248e5 | ||
|
|
6ebd401953 | ||
|
|
d69ba02851 | ||
|
|
851cca25aa | ||
|
|
ee6989a575 | ||
|
|
545eed9b0d | ||
|
|
40883386f0 | ||
|
|
0673d667f8 | ||
|
|
a2e2b7ff97 | ||
|
|
127b7d5532 | ||
|
|
a9a3389039 | ||
|
|
4b4565b19d | ||
|
|
2da6f5b3b1 | ||
|
|
d61a613911 | ||
|
|
09f2aa113b | ||
|
|
f7bb2683cb | ||
|
|
83ee986bbd | ||
|
|
c8af45537a | ||
|
|
19ac0139c1 | ||
|
|
067fa7e137 | ||
|
|
aa127b2c2b | ||
|
|
51bce6763f | ||
|
|
0e3696f20c | ||
|
|
f846126a2d | ||
|
|
825f55c0a1 | ||
|
|
9ae2381755 | ||
|
|
a37805439b | ||
|
|
253c33df33 | ||
|
|
ed0c9392a9 | ||
|
|
0098b076a1 | ||
|
|
95a4567f35 | ||
|
|
d8ddb466b0 | ||
|
|
6e5b812286 | ||
|
|
8568cab283 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
feat: Booker accepts rrHostSubsetIds to specify round robin hosts
|
||||
+4
-1
@@ -509,4 +509,7 @@ SEED_PLATFORM_OAUTH_CLIENT_SECRET=
|
||||
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
|
||||
TRIGGER_SECRET_KEY=
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
TRIGGER_DEV_PROJECT_REF=
|
||||
TRIGGER_DEV_PROJECT_REF=
|
||||
|
||||
GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid)
|
||||
LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id)
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
/apps/web/lib/core/**/* @calcom/Foundation
|
||||
/apps/web/lib/booking.ts @calcom/Foundation
|
||||
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
|
||||
/apps/web/middleware.ts @calcom/Foundation
|
||||
/apps/web/proxy.ts @calcom/Foundation
|
||||
/deploy/**/* @calcom/Foundation
|
||||
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
|
||||
@@ -28,7 +28,7 @@
|
||||
/packages/features/bookings/lib/getLuckyUser.ts @calcom/Foundation
|
||||
/packages/features/schedules/lib/slots.ts @calcom/Foundation
|
||||
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||
/packages/platform/**/* @calcom/Consumer @calcom/Foundation
|
||||
/packages/prisma/**/* @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||
|
||||
@@ -17,5 +17,6 @@ runs:
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
- name: Yarn playwright install
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
name: E2E Report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Update"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'The PR number to generate report for'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
get-pr-info:
|
||||
name: Get PR Info
|
||||
runs-on: ubuntu-latest
|
||||
# For workflow_run: only run if the triggering workflow failed
|
||||
# For workflow_dispatch: always run (inputs are provided manually)
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
pr-number: ${{ steps.get-info.outputs.pr-number }}
|
||||
head-branch: ${{ steps.get-info.outputs.head-branch }}
|
||||
has-blob-reports: ${{ steps.get-info.outputs.has-blob-reports }}
|
||||
source-run-id: ${{ steps.get-info.outputs.source-run-id }}
|
||||
source-run-number: ${{ steps.get-info.outputs.source-run-number }}
|
||||
source-run-attempt: ${{ steps.get-info.outputs.source-run-attempt }}
|
||||
source-head-sha: ${{ steps.get-info.outputs.source-head-sha }}
|
||||
steps:
|
||||
- name: Get PR and run info
|
||||
id: get-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isDispatch = context.eventName === 'workflow_dispatch';
|
||||
|
||||
if (isDispatch) {
|
||||
// For workflow_dispatch: look up everything from PR number
|
||||
const prNumber = context.payload.inputs.pr_number;
|
||||
console.log('Looking up PR:', prNumber);
|
||||
|
||||
// Get PR details
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: parseInt(prNumber)
|
||||
});
|
||||
|
||||
const headSha = pr.head.sha;
|
||||
const headBranch = pr.head.ref;
|
||||
console.log('PR head SHA:', headSha);
|
||||
console.log('PR head branch:', headBranch);
|
||||
|
||||
// Find the "PR Update" workflow
|
||||
const { data: workflows } = await github.rest.actions.listRepoWorkflows({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const prUpdateWorkflow = workflows.workflows.find(w => w.name === 'PR Update');
|
||||
if (!prUpdateWorkflow) {
|
||||
core.setFailed('Could not find "PR Update" workflow');
|
||||
return;
|
||||
}
|
||||
console.log('Found PR Update workflow ID:', prUpdateWorkflow.id);
|
||||
|
||||
// Find the most recent completed run for this branch with blob-report artifacts
|
||||
// We search by branch instead of SHA to handle cases where:
|
||||
// 1. The PR was updated after E2E tests ran (artifacts exist for older SHA)
|
||||
// 2. The current run for the SHA is still queued/in-progress
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: prUpdateWorkflow.id,
|
||||
branch: headBranch,
|
||||
status: 'completed',
|
||||
per_page: 20
|
||||
});
|
||||
|
||||
console.log('Found', runs.workflow_runs.length, 'completed runs for branch', headBranch);
|
||||
|
||||
// Find a run with blob-report artifacts
|
||||
let selectedRun = null;
|
||||
let hasBlobReports = false;
|
||||
|
||||
for (const run of runs.workflow_runs) {
|
||||
console.log('Checking run', run.id, 'conclusion:', run.conclusion);
|
||||
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: run.id
|
||||
});
|
||||
|
||||
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
|
||||
if (blobReports.length > 0) {
|
||||
console.log('Found run with blob reports:', run.id, 'artifacts:', blobReports.map(a => a.name));
|
||||
selectedRun = run;
|
||||
hasBlobReports = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedRun) {
|
||||
console.log('No runs found with blob-report artifacts');
|
||||
core.setOutput('pr-number', prNumber);
|
||||
core.setOutput('head-branch', headBranch);
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
core.setOutput('has-blob-reports', 'false');
|
||||
core.setOutput('source-run-id', '');
|
||||
core.setOutput('source-run-number', '');
|
||||
core.setOutput('source-run-attempt', '');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('pr-number', prNumber);
|
||||
core.setOutput('head-branch', headBranch);
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
core.setOutput('source-run-id', selectedRun.id.toString());
|
||||
core.setOutput('source-run-number', selectedRun.run_number.toString());
|
||||
core.setOutput('source-run-attempt', selectedRun.run_attempt.toString());
|
||||
core.setOutput('has-blob-reports', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// For workflow_run: extract from payload
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const headSha = workflowRun.head_sha;
|
||||
const headBranch = workflowRun.head_branch;
|
||||
|
||||
console.log('Head SHA:', headSha);
|
||||
console.log('Head Branch:', headBranch);
|
||||
|
||||
// Set source run info
|
||||
core.setOutput('source-run-id', workflowRun.id.toString());
|
||||
core.setOutput('source-run-number', workflowRun.run_number.toString());
|
||||
core.setOutput('source-run-attempt', workflowRun.run_attempt.toString());
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
|
||||
// Check for blob report artifacts
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: workflowRun.id
|
||||
});
|
||||
|
||||
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
|
||||
console.log('Found blob reports:', blobReports.map(a => a.name));
|
||||
core.setOutput('has-blob-reports', blobReports.length > 0 ? 'true' : 'false');
|
||||
|
||||
// Try to get PR number from workflow_run payload first
|
||||
if (workflowRun.pull_requests && workflowRun.pull_requests.length > 0) {
|
||||
const prNumber = workflowRun.pull_requests[0].number;
|
||||
console.log('PR number from payload:', prNumber);
|
||||
core.setOutput('pr-number', prNumber.toString());
|
||||
core.setOutput('head-branch', headBranch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to API lookup
|
||||
try {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: headSha
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log('No PRs found for commit');
|
||||
core.setOutput('pr-number', '');
|
||||
core.setOutput('head-branch', headBranch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find open PR or use first one
|
||||
const openPr = prs.find(pr => pr.state === 'open') || prs[0];
|
||||
console.log('PR number from API:', openPr.number);
|
||||
core.setOutput('pr-number', openPr.number.toString());
|
||||
core.setOutput('head-branch', openPr.head.ref);
|
||||
} catch (e) {
|
||||
console.log('Error fetching PR:', e);
|
||||
core.setOutput('pr-number', '');
|
||||
core.setOutput('head-branch', headBranch);
|
||||
}
|
||||
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
needs: [get-pr-info]
|
||||
if: ${{ needs.get-pr-info.outputs.has-blob-reports == 'true' && needs.get-pr-info.outputs.pr-number != '' }}
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
with:
|
||||
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
|
||||
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
|
||||
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
|
||||
source_head_sha: ${{ needs.get-pr-info.outputs.source-head-sha }}
|
||||
# No secrets: inherit - merge-reports only needs the default GITHUB_TOKEN for artifact download
|
||||
|
||||
publish-report:
|
||||
name: Publish HTML report
|
||||
needs: [get-pr-info, merge-reports]
|
||||
if: ${{ needs.get-pr-info.outputs.pr-number != '' }}
|
||||
uses: ./.github/workflows/publish-report.yml
|
||||
with:
|
||||
head_branch: ${{ needs.get-pr-info.outputs.head-branch }}
|
||||
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
|
||||
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
|
||||
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
|
||||
pr_number: ${{ needs.get-pr-info.outputs.pr-number }}
|
||||
secrets: inherit
|
||||
@@ -1,17 +1,37 @@
|
||||
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
source_run_id:
|
||||
description: 'The run ID to download artifacts from (for workflow_run triggered calls)'
|
||||
required: true
|
||||
type: string
|
||||
source_run_number:
|
||||
description: 'The run number for artifact naming'
|
||||
required: true
|
||||
type: string
|
||||
source_run_attempt:
|
||||
description: 'The run attempt for artifact naming'
|
||||
required: true
|
||||
type: string
|
||||
source_head_sha:
|
||||
description: 'The head SHA to checkout for Playwright version consistency'
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
merge-reports:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
with:
|
||||
ref: ${{ inputs.source_head_sha }}
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
@@ -20,6 +40,6 @@ jobs:
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
+76
-88
@@ -2,7 +2,7 @@ name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- main
|
||||
- gh-actions-test-branch
|
||||
@@ -17,8 +17,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
@@ -26,43 +26,44 @@ jobs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
has_companion: ${{ steps.filter.outputs.has_companion }}
|
||||
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
|
||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
predicate-quantifier: "every"
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(companion/**|**.md|**.mdx|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json|i18n.lock)"
|
||||
- '**'
|
||||
- '!companion/**'
|
||||
- '!**/*.md'
|
||||
- '!**/*.mdx'
|
||||
- '!.github/CODEOWNERS'
|
||||
- '!docs/**'
|
||||
- '!help/**'
|
||||
- '!apps/web/public/static/locales/**/common.json'
|
||||
- '!i18n.lock'
|
||||
has_companion:
|
||||
- "companion/**"
|
||||
- name: Get Latest Commit SHA
|
||||
id: get_sha
|
||||
run: |
|
||||
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
check-label:
|
||||
needs: [changes]
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
name: Check for E2E label
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || github.event.label.name == 'ready-for-e2e') }}
|
||||
steps:
|
||||
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||
id: check-if-pr-has-label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let labels = [];
|
||||
let prNumber = null;
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
labels = context.payload.pull_request.labels;
|
||||
prNumber = context.payload.pull_request.number;
|
||||
} else {
|
||||
try {
|
||||
const sha = '${{ needs.changes.outputs.commit-sha }}';
|
||||
const sha = '${{ steps.get_sha.outputs.commit-sha }}';
|
||||
console.log('sha', sha);
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
@@ -76,132 +77,149 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = prs[0];
|
||||
console.log(`PR number: ${pr.number}`);
|
||||
console.log(`PR title: ${pr.title}`);
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR URL: ${pr.html_url}`);
|
||||
|
||||
labels = pr.labels;
|
||||
prNumber = prs[0].number;
|
||||
}
|
||||
catch (e) {
|
||||
core.setOutput('run-e2e', false);
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Always fetch fresh PR data to get current labels
|
||||
// This avoids stale label data from event payloads
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
console.log(`PR number: ${pr.number}`);
|
||||
console.log(`PR title: ${pr.title}`);
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR URL: ${pr.html_url}`);
|
||||
|
||||
labels = pr.labels;
|
||||
}
|
||||
catch (e) {
|
||||
core.setOutput('run-e2e', false);
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
|
||||
console.log('Found the label?', labelFound);
|
||||
core.setOutput('run-e2e', labelFound);
|
||||
|
||||
deps:
|
||||
name: Install dependencies
|
||||
needs: [changes, check-label]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/yarn-install.yml
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
unit-test:
|
||||
name: Tests
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v2-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-atoms:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/atoms-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-docs:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/docs-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-companion:
|
||||
name: Companion builds
|
||||
needs: [changes, check-label]
|
||||
if: needs.changes.outputs.has_companion == 'true'
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.has_companion == 'true'
|
||||
uses: ./.github/workflows/companion-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, deps]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-api-v2:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-api-v2.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [prepare, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -211,44 +229,14 @@ jobs:
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
secrets: inherit
|
||||
|
||||
publish-report:
|
||||
name: Publish HTML report
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
needs: [changes, check-label, merge-reports]
|
||||
uses: ./.github/workflows/publish-report.yml
|
||||
secrets: inherit
|
||||
|
||||
cleanup-report:
|
||||
name: Cleanup HTML report
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && (github.event.pull_request.merged == true || github.event.pull_request.state == 'closed') }}
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
needs: [changes, check-label, publish-report]
|
||||
uses: ./.github/workflows/cleanup-report.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs:
|
||||
[
|
||||
changes,
|
||||
prepare,
|
||||
lint,
|
||||
type-check,
|
||||
unit-test,
|
||||
integration-test,
|
||||
check-label,
|
||||
build,
|
||||
build-api-v1,
|
||||
build-api-v2,
|
||||
@@ -268,7 +256,7 @@ jobs:
|
||||
run: exit 1
|
||||
if: |
|
||||
(
|
||||
needs.changes.outputs.has-files-requiring-all-checks == 'true' &&
|
||||
needs.prepare.outputs.has-files-requiring-all-checks == 'true' &&
|
||||
(
|
||||
needs.lint.result != 'success' ||
|
||||
needs.type-check.result != 'success' ||
|
||||
@@ -287,6 +275,6 @@ jobs:
|
||||
)
|
||||
) ||
|
||||
(
|
||||
needs.changes.outputs.has_companion == 'true' &&
|
||||
needs.prepare.outputs.has_companion == 'true' &&
|
||||
needs.build-companion.result != 'success'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
head_branch:
|
||||
description: 'The head branch name for report path'
|
||||
required: true
|
||||
type: string
|
||||
source_run_id:
|
||||
description: 'The source run ID for report path'
|
||||
required: true
|
||||
type: string
|
||||
source_run_number:
|
||||
description: 'The source run number for artifact naming and comment'
|
||||
required: true
|
||||
type: string
|
||||
source_run_attempt:
|
||||
description: 'The source run attempt for artifact naming and comment'
|
||||
required: true
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'The PR number to post comment on'
|
||||
required: true
|
||||
type: string
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
@@ -10,8 +31,8 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
# Unique URL path for each workflow run attempt
|
||||
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
|
||||
HTML_REPORT_URL_PATH: reports/${{ inputs.head_branch }}/${{ inputs.source_run_id }}/${{ inputs.source_run_attempt }}
|
||||
BRANCH_REPORTS_DIR: reports/${{ inputs.head_branch }}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
@@ -45,11 +66,11 @@ jobs:
|
||||
run: |
|
||||
rm -rf $BRANCH_REPORTS_DIR
|
||||
git add .
|
||||
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
|
||||
git commit -m "workflow: remove all reports for branch ${{ inputs.head_branch }}"
|
||||
- name: Download zipped HTML report
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
|
||||
path: ${{ env.HTML_REPORT_URL_PATH }}
|
||||
- name: Push HTML Report
|
||||
timeout-minutes: 3
|
||||
@@ -57,7 +78,7 @@ jobs:
|
||||
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
|
||||
git commit -m "workflow: add HTML report for run-id ${{ inputs.source_run_id }} (attempt: ${{ inputs.source_run_attempt }})"
|
||||
|
||||
while true; do
|
||||
git pull --rebase
|
||||
@@ -82,18 +103,18 @@ jobs:
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
issue-number: ${{ inputs.pr_number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: <!-- GENERATED-E2E-RESULTS -->
|
||||
- name: Create comment
|
||||
if: steps.fc.outputs.comment-id == ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
issue-number: ${{ inputs.pr_number }}
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- name: Update comment
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
@@ -103,4 +124,4 @@ jobs:
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
|
||||
-541
File diff suppressed because one or more lines are too long
-550
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-873
File diff suppressed because one or more lines are too long
+942
File diff suppressed because one or more lines are too long
+5
-9
@@ -1,11 +1,7 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
|
||||
@@ -255,7 +255,55 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
### packages/features Import Restrictions
|
||||
|
||||
Files in `packages/features/**` should NOT import from `trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files, and let the tRPC middleware handle the conversion.
|
||||
Files in `packages/features/**` should NOT import from `@calcom/trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files, and let the tRPC middleware handle the conversion.
|
||||
|
||||
**Architecture: packages/features vs apps/web/modules**
|
||||
|
||||
The `packages/features` package should contain only framework-agnostic code:
|
||||
- Repositories (data access layer)
|
||||
- Services (business logic)
|
||||
- Core utilities and helpers
|
||||
- Types and interfaces
|
||||
|
||||
Web-specific code, particularly anything that uses tRPC, should live in `apps/web/modules/...`. This includes:
|
||||
- React hooks that use tRPC queries/mutations
|
||||
- tRPC-specific utilities
|
||||
- Web-only UI components that depend on tRPC
|
||||
|
||||
**Example:**
|
||||
|
||||
If you have a feature called `feature-opt-in`:
|
||||
|
||||
```
|
||||
packages/features/feature-opt-in/
|
||||
├── repository/
|
||||
│ └── FeatureOptInRepository.ts # Data access - OK here
|
||||
├── service/
|
||||
│ └── FeatureOptInService.ts # Business logic - OK here
|
||||
└── types.ts # Types - OK here
|
||||
|
||||
apps/web/modules/feature-opt-in/
|
||||
└── hooks/
|
||||
└── useFeatureOptIn.ts # tRPC hook - MUST be here, not in packages/features
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - tRPC hook in packages/features
|
||||
// packages/features/feature-opt-in/hooks/useFeatureOptIn.ts
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
export function useFeatureOptIn() {
|
||||
return trpc.viewer.featureOptIn.useQuery();
|
||||
}
|
||||
|
||||
// ✅ Good - tRPC hook in apps/web/modules
|
||||
// apps/web/modules/feature-opt-in/hooks/useFeatureOptIn.ts
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
export function useFeatureOptIn() {
|
||||
return trpc.viewer.featureOptIn.useQuery();
|
||||
}
|
||||
```
|
||||
|
||||
This separation ensures that `packages/features` remains portable and can be used by other apps (like `apps/api/v2`) without pulling in web-specific dependencies like tRPC React hooks.
|
||||
|
||||
## Basic Performance Guidelines
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export const schemaBookingReadPublic = Booking.extend({
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
timeZone: true,
|
||||
// Note: timeZone is not a field on Booking model - it's in nested attendees/user objects
|
||||
attendees: true,
|
||||
user: true,
|
||||
eventType: true,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from "zod";
|
||||
|
||||
import { DestinationCalendarSchema } from "@calcom/prisma/zod/modelSchema/DestinationCalendarSchema";
|
||||
|
||||
// Note: bookingId is not a field on DestinationCalendar model in Prisma schema
|
||||
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendarSchema.pick({
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
|
||||
@@ -38,11 +38,11 @@ export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendar
|
||||
schemaDestinationCalendarEditParams
|
||||
);
|
||||
|
||||
// Note: bookingId is not a field on DestinationCalendar model in Prisma schema
|
||||
export const schemaDestinationCalendarReadPublic = DestinationCalendarSchema.pick({
|
||||
id: true,
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
});
|
||||
|
||||
@@ -151,6 +151,7 @@ export const schemaUserCreateBodyParams = schemaUserBaseBodyParams
|
||||
.strict();
|
||||
|
||||
// @note: These are the values that are always returned when reading a user
|
||||
// Note: We pick avatarUrl from the Prisma schema and extend with avatar for API v1 backward compatibility
|
||||
export const schemaUserReadPublic = UserSchema.pick({
|
||||
id: true,
|
||||
username: true,
|
||||
@@ -158,7 +159,7 @@ export const schemaUserReadPublic = UserSchema.pick({
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
avatarUrl: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
endTime: true,
|
||||
@@ -176,6 +177,9 @@ export const schemaUserReadPublic = UserSchema.pick({
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
role: true,
|
||||
}).extend({
|
||||
// API v1 backward compatibility: expose avatarUrl as avatar
|
||||
avatar: UserSchema.shape.avatarUrl,
|
||||
});
|
||||
|
||||
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin");
|
||||
@@ -6,6 +7,7 @@ const plugins = [withAxiom];
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
turbopack: {},
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/dayjs",
|
||||
@@ -94,7 +96,7 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
plugins.push((nextConfig) =>
|
||||
withSentryConfig(nextConfig, {
|
||||
autoInstrumentServerFunctions: true,
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@calcom/types": "workspace:*",
|
||||
"node-mocks-http": "^1.11.0"
|
||||
"node-mocks-http": "1.16.2",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/app-store": "workspace:*",
|
||||
@@ -35,14 +36,13 @@
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "15.4.10",
|
||||
"next": "16.1.0",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
"next-validations": "^0.2.0",
|
||||
"typescript": "^5.9.0-beta",
|
||||
"tzdata": "^1.0.30",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "3.25.76"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function middleware(_request: NextRequest) {
|
||||
function proxy(_request: NextRequest) {
|
||||
// Matcher guarantees the last segment is one of the blocked segments, so we can
|
||||
// immediately return a 403 without further path checks.
|
||||
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
|
||||
@@ -17,3 +17,5 @@ export const config = {
|
||||
"/:path*/_auth-middleware",
|
||||
],
|
||||
};
|
||||
|
||||
export default proxy;
|
||||
+19
-19
@@ -16,7 +16,7 @@
|
||||
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
||||
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build && yarn workspace @calcom/trpc build:server",
|
||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
@@ -43,7 +43,6 @@
|
||||
"@calcom/platform-types": "workspace:*",
|
||||
"@calcom/platform-utils": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@golevelup/ts-jest": "^0.4.0",
|
||||
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||
"@nest-lab/throttler-storage-redis": "1.0.0",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
@@ -60,59 +59,60 @@
|
||||
"@sentry/node": "^9.15.0",
|
||||
"@sentry/profiling-node": "^9.15.0",
|
||||
"@snyk/protect": "latest",
|
||||
"axios": "^1.9.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.12.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv": "16.6.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"googleapis": "^84.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsforce": "^1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
"nest-winston": "^1.9.4",
|
||||
"next-auth": "^4.22.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"qs-stringify": "^1.2.1",
|
||||
"querystring": "^0.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"rxjs": "7.8.2",
|
||||
"stripe": "^15.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.13.0",
|
||||
"winston-transport": "^4.7.0",
|
||||
"zod": "^3.22.4"
|
||||
"winston-transport": "4.9.0",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@golevelup/ts-jest": "^0.4.0",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/schematics": "10.1.1",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6",
|
||||
"@typescript-eslint/parser": "^6",
|
||||
"concurrently": "^9.1.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"prettier": "^2.8.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-jest": "29.1.4",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^5.9.0-beta"
|
||||
"ts-node": "10.9.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "../../../packages/prisma/schema.prisma"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingLocationController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-location.controller";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
|
||||
@@ -100,8 +102,13 @@ import { Module } from "@nestjs/common";
|
||||
CalVideoService,
|
||||
CalVideoOutputService,
|
||||
BookingPbacGuard,
|
||||
BookingLocationService_2024_08_13,
|
||||
],
|
||||
controllers: [
|
||||
BookingsController_2024_08_13,
|
||||
BookingGuestsController_2024_08_13,
|
||||
BookingLocationController_2024_08_13,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
})
|
||||
export class BookingsModule_2024_08_13 {}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.output";
|
||||
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Controller, Patch, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/bookings/:bookingUid/location",
|
||||
version: VERSION_2024_08_13_VALUE,
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingLocationController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingLocationController_2024_08_13");
|
||||
|
||||
constructor(private readonly bookingLocationService: BookingLocationService_2024_08_13) {}
|
||||
|
||||
@Patch("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Update booking location for an existing booking",
|
||||
description: `**Current Limitation:** Updating a booking location will update the location in Cal.com, but the corresponding Calendar event will not be updated automatically. The old location will persist in the Calendar event. This is a known limitation that will be addressed in a future update.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>`,
|
||||
})
|
||||
async updateBookingLocation(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: UpdateBookingLocationInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<UpdateBookingLocationOutput_2024_08_13> {
|
||||
const booking = await this.bookingLocationService.updateBookingLocation(bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
+17
-7
@@ -16,9 +16,9 @@ import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.rep
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
@@ -73,6 +73,7 @@ type EmailSetup = {
|
||||
eventTypeId: number;
|
||||
createdBookingUid: string;
|
||||
rescheduledBookingUid: string;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
@@ -85,6 +86,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let emailsEnabledSetup: EmailSetup;
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
@@ -94,12 +96,9 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
authEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
@@ -112,6 +111,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `confirm-emails-2024-08-13-organization-${randomString()}`,
|
||||
@@ -152,6 +152,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsEnabled.id);
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
@@ -174,6 +176,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
eventTypeId: event.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
accessToken: tokens.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +191,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsDisabled.id);
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
@@ -209,6 +214,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
eventTypeId: event.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
accessToken: tokens.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,6 +283,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
@@ -331,6 +338,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
@@ -391,6 +399,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
@@ -508,6 +517,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
ERROR_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
|
||||
type TestUser = {
|
||||
id: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
eventTypeId: number;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 update booking location", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `update-booking-location-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("PATCH /v2/bookings/:bookingUid/location", () => {
|
||||
it("should return 401 when updating location without authentication", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: "https://unauthenticated.example.com" })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
describe("can update booking with different locations", () => {
|
||||
const address = "123 Main St";
|
||||
const link = "https://cal.com/join/123456";
|
||||
const phone = "+37121999999";
|
||||
|
||||
let booking: Booking;
|
||||
let bookingId: number;
|
||||
let bookingUid: string;
|
||||
let eventTypeWithAllLocationsId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const createdEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "book using any location",
|
||||
slug: `book-using-any-location-${randomString()}`,
|
||||
length: 15,
|
||||
locations: [
|
||||
{ type: "integrations:daily" },
|
||||
{ type: "inPerson", address: "123 Main St", displayLocationPublicly: true },
|
||||
{ type: "link", link: "https://cal.com/join/123456", displayLocationPublicly: true },
|
||||
{ type: "userPhone", hostPhoneNumber: "+37121999999", displayLocationPublicly: true },
|
||||
{ type: "attendeeInPerson" },
|
||||
{ type: "phone" },
|
||||
{ type: "somewhereElse" },
|
||||
],
|
||||
},
|
||||
testSetup.organizer.id
|
||||
);
|
||||
|
||||
eventTypeWithAllLocationsId = createdEventType.id;
|
||||
|
||||
booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2048-08-14T09:00:00.000Z",
|
||||
endTime: "2048-08-14T10:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeWithAllLocationsId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: testSetup.organizer.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
bookingUid = booking.uid;
|
||||
bookingId = booking.id;
|
||||
});
|
||||
|
||||
it("can update location to type address", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "address",
|
||||
address: address,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(address);
|
||||
});
|
||||
|
||||
it("can update location to type link", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "link",
|
||||
link: link,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(link);
|
||||
});
|
||||
|
||||
it("can update location to type phone", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "phone",
|
||||
phone: phone,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(phone);
|
||||
});
|
||||
|
||||
it("can update location to type attendeeAddress", async () => {
|
||||
const attendeeAddress = "123 Example St, City, Valhalla";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeeAddress",
|
||||
address: attendeeAddress,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeeAddress);
|
||||
});
|
||||
|
||||
it("can update location to type attendeePhone", async () => {
|
||||
const attendeePhone = "+37120993151";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeePhone",
|
||||
phone: attendeePhone,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeePhone);
|
||||
});
|
||||
|
||||
it("can update location to type attendeeDefined", async () => {
|
||||
const attendeeDefinedLocation = "namek 100";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeeDefined",
|
||||
location: attendeeDefinedLocation,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeeDefinedLocation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (responseDataIsBooking(booking)) {
|
||||
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return booking unchanged when no location payload is provided", async () => {
|
||||
const initialLocation = `https://initial-${randomString()}.example.com`;
|
||||
const { bookingUid } = await createBooking(initialLocation);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: UpdateBookingLocationOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(responseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
expect(responseBody.data.location).toEqual(initialLocation);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user attempts to update booking location", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: { type: "link", link: "https://non-organizer.example.com" } })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should return 400 when invalid location payload is provided", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: { type: "invalid-location-type" } })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.status).toEqual(ERROR_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(testSetup.organizer.id, testSetup.organizer.email);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `update-booking-location-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUser = await userRepositoryFixture.create({
|
||||
email: `update-booking-location-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedTokens = await tokensRepositoryFixture.createTokens(unrelatedUser.id, oAuthClient.id);
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `update-booking-location-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, userSchedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `update-booking-location-event-type-${randomString()}`,
|
||||
slug: `update-booking-location-event-type-${randomString()}`,
|
||||
length: 45,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
id: organizerUser.id,
|
||||
email: organizerUser.email,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
id: unrelatedUser.id,
|
||||
email: unrelatedUser.email,
|
||||
accessToken: unrelatedTokens.accessToken,
|
||||
refreshToken: unrelatedTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:4444"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: true,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
return oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
}
|
||||
|
||||
async function createBooking(location?: string) {
|
||||
const bookingLocation = location ?? `https://initial-${randomString()}.example.com`;
|
||||
const bookingUid = `booking-uid-${randomString(10)}`;
|
||||
|
||||
await bookingsRepositoryFixture.create({
|
||||
uid: bookingUid,
|
||||
title: "booking title",
|
||||
startTime: new Date(Date.UTC(2035, 0, 9, 13, 0, 0)).toISOString(),
|
||||
endTime: new Date(Date.UTC(2035, 0, 9, 13, 45, 0)).toISOString(),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: testSetup.eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
location: bookingLocation,
|
||||
user: {
|
||||
connect: {
|
||||
id: testSetup.organizer.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bookingUid,
|
||||
location: bookingLocation,
|
||||
};
|
||||
}
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
export class UpdateBookingLocationOutput_2024_08_13 extends GetBookingOutput_2024_08_13 {}
|
||||
@@ -206,4 +206,14 @@ export class BookingsRepository_2024_08_13 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateBooking(bookingUid: string, body: Prisma.BookingUpdateInput) {
|
||||
return this.dbWrite.prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: body,
|
||||
select: { uid: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, Logger, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
import type {
|
||||
UpdateBookingLocationInput_2024_08_13,
|
||||
BookingInputLocation_2024_08_13,
|
||||
UpdateBookingInputLocation_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class BookingLocationService_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingLocationService_2024_08_13");
|
||||
|
||||
constructor(
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly bookingsService: BookingsService_2024_08_13,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly inputService: InputBookingsService_2024_08_13,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly eventTypeAccessService: EventTypeAccessService
|
||||
) {}
|
||||
|
||||
async updateBookingLocation(
|
||||
bookingUid: string,
|
||||
input: UpdateBookingLocationInput_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
) {
|
||||
const existingBooking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
|
||||
if (!existingBooking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
|
||||
if (existingBooking.eventTypeId && existingBooking.eventType) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
|
||||
existingBooking.eventTypeId
|
||||
);
|
||||
if (eventType) {
|
||||
const isAllowed = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(user, eventType);
|
||||
if (!isAllowed) {
|
||||
throw new ForbiddenException(
|
||||
"User is not authorized to update this booking location. User must be the event type owner, host, team admin or owner, or org admin or owner."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { location } = input;
|
||||
|
||||
if (location) {
|
||||
return await this.updateLocation(existingBooking, location, user);
|
||||
}
|
||||
|
||||
return this.bookingsService.getBooking(existingBooking.uid, user);
|
||||
}
|
||||
|
||||
private async updateLocation(
|
||||
existingBooking: Booking,
|
||||
inputLocation: UpdateBookingInputLocation_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
) {
|
||||
const bookingUid = existingBooking.uid;
|
||||
const bookingLocation = this.getLocationValue(inputLocation) ?? existingBooking.location;
|
||||
|
||||
if (!existingBooking.userId) {
|
||||
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
if (!existingBooking.eventTypeId) {
|
||||
throw new NotFoundException(`No event type found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
const existingBookingHost = await this.usersRepository.findById(existingBooking.userId);
|
||||
|
||||
if (!existingBookingHost) {
|
||||
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
const bookingFieldsLocation = this.inputService.transformLocation(
|
||||
inputLocation as BookingInputLocation_2024_08_13
|
||||
);
|
||||
|
||||
const responses = (existingBooking.responses || {}) as Record<string, unknown>;
|
||||
const { location: _existingLocation, ...rest } = responses;
|
||||
|
||||
const updatedBookingResponses = { ...rest, location: bookingFieldsLocation };
|
||||
|
||||
const updatedBooking = await this.bookingsRepository.updateBooking(bookingUid, {
|
||||
location: bookingLocation,
|
||||
responses: updatedBookingResponses,
|
||||
});
|
||||
|
||||
return this.bookingsService.getBooking(updatedBooking.uid, user);
|
||||
}
|
||||
|
||||
private getLocationValue(loc: UpdateBookingInputLocation_2024_08_13): string | undefined {
|
||||
if (loc.type === "address") return loc.address;
|
||||
if (loc.type === "link") return loc.link;
|
||||
if (loc.type === "phone") return loc.phone;
|
||||
if (loc.type === "attendeeAddress") return loc.address;
|
||||
if (loc.type === "attendeePhone") return loc.phone;
|
||||
if (loc.type === "attendeeDefined") return loc.location;
|
||||
|
||||
this.logger.log(
|
||||
`Booking location service getLocationValue - loc ${JSON.stringify(
|
||||
loc
|
||||
)} was passed but the type is not supported.`
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+103
-1
@@ -167,7 +167,7 @@ describe("Organizations Users Endpoints", () => {
|
||||
|
||||
await userRepositoryFixture.create({
|
||||
email: nonMemberEmail,
|
||||
username: "non-member",
|
||||
username: `non-member-${randomString()}`,
|
||||
});
|
||||
|
||||
const orgMembers = await Promise.all(
|
||||
@@ -379,6 +379,108 @@ describe("Organizations Users Endpoints", () => {
|
||||
createdUser = userData;
|
||||
});
|
||||
|
||||
it("creates a new org user with username and avatarUrl", async () => {
|
||||
const shortRandom = randomString().substring(0, 8);
|
||||
const testUsername = `user${shortRandom}`;
|
||||
const testEmail = `org-user-${shortRandom}@api.com`;
|
||||
const githubAvatarUrl = "https://avatars.githubusercontent.com/u/583231?v=4";
|
||||
|
||||
const newOrgUserWithUsernameAndAvatar: CreateUserInput = {
|
||||
email: testEmail,
|
||||
username: testUsername,
|
||||
avatarUrl: githubAvatarUrl,
|
||||
bio,
|
||||
metadata,
|
||||
timeZone: "America/Sao_Paulo",
|
||||
timeFormat: 24,
|
||||
locale: "pt",
|
||||
};
|
||||
|
||||
const emailSpy = jest
|
||||
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users`)
|
||||
.send(newOrgUserWithUsernameAndAvatar)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
// Verify response status
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
|
||||
// Verify all user fields are correctly set
|
||||
expect(userData.email).toBe(testEmail);
|
||||
expect(userData.username).toBe(testUsername);
|
||||
expect(userData.avatarUrl).toBe(githubAvatarUrl);
|
||||
expect(userData.bio).toBe(bio);
|
||||
expect(userData.metadata).toEqual(metadata);
|
||||
expect(userData.timeZone).toBe("America/Sao_Paulo");
|
||||
expect(userData.timeFormat).toBe(24);
|
||||
expect(userData.locale).toBe("pt");
|
||||
|
||||
// Verify email was sent with correct parameters (using email, not username)
|
||||
expect(emailSpy).toHaveBeenCalledWith({
|
||||
usernameOrEmail: testEmail,
|
||||
orgName: org.name,
|
||||
orgId: org.id,
|
||||
inviterName: userEmail,
|
||||
locale: "pt",
|
||||
});
|
||||
|
||||
// Clean up the created user
|
||||
await userRepositoryFixture.deleteByEmail(testEmail);
|
||||
});
|
||||
|
||||
it("creates a new org user with avatarUrl as base64 image", async () => {
|
||||
const shortRandom = randomString().substring(0, 8);
|
||||
const testEmail = `org-b64-${shortRandom}@api.com`;
|
||||
const base64Avatar =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
||||
|
||||
const newOrgUserWithBase64Avatar: CreateUserInput = {
|
||||
email: testEmail,
|
||||
avatarUrl: base64Avatar,
|
||||
bio,
|
||||
metadata,
|
||||
};
|
||||
|
||||
const emailSpy = jest
|
||||
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users`)
|
||||
.send(newOrgUserWithBase64Avatar)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
// Verify response status
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
|
||||
// Verify base64 avatar is accepted and stored
|
||||
expect(userData.email).toBe(testEmail);
|
||||
expect(userData.avatarUrl).toBe(base64Avatar);
|
||||
expect(userData.bio).toBe(bio);
|
||||
expect(userData.metadata).toEqual(metadata);
|
||||
|
||||
// Verify email was sent
|
||||
expect(emailSpy).toHaveBeenCalledWith({
|
||||
usernameOrEmail: testEmail,
|
||||
orgName: org.name,
|
||||
orgId: org.id,
|
||||
inviterName: userEmail,
|
||||
locale: null,
|
||||
});
|
||||
|
||||
// Clean up the created user
|
||||
await userRepositoryFixture.deleteByEmail(testEmail);
|
||||
});
|
||||
|
||||
it("should delete an org user", async () => {
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/users/${createdUser.id}`)
|
||||
|
||||
+3
-5
@@ -68,13 +68,11 @@ export class OrganizationsUsersService {
|
||||
await this.checkForUsernameConflicts(org.id, userCreateBody.username);
|
||||
}
|
||||
|
||||
const usernameOrEmail = userCreateBody.username ? userCreateBody.username : userCreateBody.email;
|
||||
|
||||
// Create new org user
|
||||
const createdUserCall = await createNewUsersConnectToOrgIfExists({
|
||||
invitations: [
|
||||
{
|
||||
usernameOrEmail: usernameOrEmail,
|
||||
usernameOrEmail: userCreateBody.email,
|
||||
role: userCreateBody.organizationRole,
|
||||
},
|
||||
],
|
||||
@@ -84,7 +82,7 @@ export class OrganizationsUsersService {
|
||||
parentId: null,
|
||||
autoAcceptEmailDomain: "not-required-for-this-endpoint",
|
||||
orgConnectInfoByUsernameOrEmail: {
|
||||
[usernameOrEmail]: {
|
||||
[userCreateBody.email]: {
|
||||
orgId: org.id,
|
||||
autoAccept: userCreateBody.autoAccept,
|
||||
},
|
||||
@@ -106,7 +104,7 @@ export class OrganizationsUsersService {
|
||||
|
||||
// Need to send email to new user to create password
|
||||
await this.emailService.sendSignupToOrganizationEmail({
|
||||
usernameOrEmail,
|
||||
usernameOrEmail: userCreateBody.email,
|
||||
orgName: org.name,
|
||||
orgId: org.id,
|
||||
locale: user?.locale,
|
||||
|
||||
+51
@@ -307,6 +307,57 @@ describe("Teams Memberships Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign team wide events when membership transitions from accepted=false to accepted=true via PATCH", async () => {
|
||||
const pendingUserEmail = `pending-user-${randomString()}@api.com`;
|
||||
const pendingUser = await userRepositoryFixture.create({
|
||||
email: pendingUserEmail,
|
||||
username: pendingUserEmail,
|
||||
bio,
|
||||
metadata,
|
||||
});
|
||||
|
||||
const createPendingMembershipBody: CreateTeamMembershipInput = {
|
||||
userId: pendingUser.id,
|
||||
accepted: false,
|
||||
role: "MEMBER",
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${team.id}/memberships`)
|
||||
.send(createPendingMembershipBody)
|
||||
.expect(201);
|
||||
|
||||
const pendingMembership: TeamMembershipOutput = createResponse.body.data;
|
||||
expect(pendingMembership.accepted).toEqual(false);
|
||||
|
||||
const teamEventTypesBeforePatch = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
|
||||
const collectiveEventBeforePatch = teamEventTypesBeforePatch?.find(
|
||||
(eventType) => eventType.slug === teamEventType.slug
|
||||
);
|
||||
const userHostBeforePatch = collectiveEventBeforePatch?.hosts.find((host) => host.userId === pendingUser.id);
|
||||
expect(userHostBeforePatch).toBeFalsy();
|
||||
|
||||
const updateToAcceptedBody: UpdateTeamMembershipInput = {
|
||||
accepted: true,
|
||||
};
|
||||
|
||||
const patchResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
|
||||
.send(updateToAcceptedBody)
|
||||
.expect(200);
|
||||
|
||||
const acceptedMembership: TeamMembershipOutput = patchResponse.body.data;
|
||||
expect(acceptedMembership.accepted).toEqual(true);
|
||||
|
||||
await userHasCorrectEventTypes(pendingUser.id);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
|
||||
.expect(200);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(pendingUserEmail);
|
||||
});
|
||||
|
||||
it("should delete the membership of the org's team we created via api", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
|
||||
+3
-3
@@ -118,8 +118,6 @@ export class TeamsMembershipsController {
|
||||
@Param("membershipId", ParseIntPipe) membershipId: number,
|
||||
@Body() body: UpdateTeamMembershipInput
|
||||
): Promise<UpdateTeamMembershipOutput> {
|
||||
const membership = await this.teamsMembershipsService.updateTeamMembership(teamId, membershipId, body);
|
||||
|
||||
const currentMembership = await this.teamsMembershipsService.getTeamMembership(teamId, membershipId);
|
||||
|
||||
const updatedMembership = await this.teamsMembershipsService.updateTeamMembership(
|
||||
@@ -127,6 +125,7 @@ export class TeamsMembershipsController {
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
@@ -134,9 +133,10 @@ export class TeamsMembershipsController {
|
||||
this.logger.error("Could not update new team member eventTypes", err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
data: plainToClass(TeamMembershipOutput, updatedMembership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "next-auth";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { User } from "@calcom/prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { IsOptional, Validate, validateSync } from "class-validator";
|
||||
|
||||
import { AvatarValidator } from "./avatarValidator";
|
||||
|
||||
// Mock DTO for testing the validator
|
||||
class MockAvatarDto {
|
||||
@IsOptional()
|
||||
@Validate(AvatarValidator)
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
// Factory function to create DTOs with avatar
|
||||
const createAvatarDto = (avatar?: string): MockAvatarDto => {
|
||||
const dto = new MockAvatarDto();
|
||||
dto.avatar = avatar;
|
||||
return dto;
|
||||
};
|
||||
|
||||
describe("AvatarValidator", () => {
|
||||
describe("when value is undefined", () => {
|
||||
it("accepts undefined values", () => {
|
||||
const dto = createAvatarDto(undefined);
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is a URL", () => {
|
||||
it("rejects HTTP URLs (HTTPS only for security)", () => {
|
||||
const dto = createAvatarDto("http://example.com/avatar.jpg");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("accepts valid HTTPS URLs", () => {
|
||||
const dto = createAvatarDto("https://avatars.githubusercontent.com/u/583231?v=4");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts URLs with query parameters", () => {
|
||||
const dto = createAvatarDto("https://example.com/avatar.jpg?size=256&format=png");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts URLs with special characters in path", () => {
|
||||
const dto = createAvatarDto("https://cdn.example.com/users/john-doe/avatar_2025.jpg");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects URLs without protocol", () => {
|
||||
const dto = createAvatarDto("example.com/avatar.jpg");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects FTP protocol for security", () => {
|
||||
const dto = createAvatarDto("ftp://example.com/avatar.jpg");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects file protocol for security", () => {
|
||||
const dto = createAvatarDto("file:///path/to/avatar.jpg");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects javascript protocol to prevent XSS", () => {
|
||||
const dto = createAvatarDto("javascript:alert('XSS')");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is base64 encoded image", () => {
|
||||
it("accepts valid PNG base64", () => {
|
||||
const dto = createAvatarDto(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
);
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts valid JPEG base64", () => {
|
||||
const dto = createAvatarDto("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA==");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts valid GIF base64", () => {
|
||||
const dto = createAvatarDto(
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
);
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts valid WEBP base64", () => {
|
||||
const dto = createAvatarDto(
|
||||
"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA="
|
||||
);
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects base64 with non-image MIME type", () => {
|
||||
const dto = createAvatarDto("data:text/plain;base64,SGVsbG8gV29ybGQ=");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects malformed base64 data URL", () => {
|
||||
const dto = createAvatarDto("data:image/png;base64,INVALID_BASE64_@#$%");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects base64 with application/json MIME type", () => {
|
||||
const dto = createAvatarDto("data:application/json;base64,eyJmb28iOiJiYXIifQ==");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is invalid", () => {
|
||||
it("rejects plain text strings", () => {
|
||||
const dto = createAvatarDto("not a url or base64");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects empty strings (use null to reset avatar)", () => {
|
||||
const dto = createAvatarDto("");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
|
||||
it("rejects whitespace-only strings", () => {
|
||||
const dto = createAvatarDto(" ");
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].constraints).toHaveProperty("avatarValidator");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,30 @@
|
||||
import { ValidatorConstraint } from "class-validator";
|
||||
import type { ValidatorConstraintInterface } from "class-validator";
|
||||
|
||||
const BASE64_IMAGE_REGEX =
|
||||
/^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
|
||||
@ValidatorConstraint({ name: "avatarValidator", async: false })
|
||||
export class AvatarValidator implements ValidatorConstraintInterface {
|
||||
validate(avatarString: string) {
|
||||
// Checks if avatar string is a valid base 64 image
|
||||
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
return regex.test(avatarString);
|
||||
validate(avatarString: string): boolean {
|
||||
if (!avatarString?.trim()) return false;
|
||||
return this.isValidUrl(avatarString) || this.isValidBase64Image(avatarString);
|
||||
}
|
||||
|
||||
private isValidUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isValidBase64Image(value: string): boolean {
|
||||
return BASE64_IMAGE_REGEX.test(value);
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return "avatarUrl must be a valid HTTPS URL (HTTP not allowed) or base64 encoded image";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function revalidateTeamDataCache({
|
||||
teamSlug: string;
|
||||
orgSlug: string | null;
|
||||
}) {
|
||||
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`);
|
||||
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`, "max");
|
||||
}
|
||||
|
||||
// Coupled with `getCachedTeamEventType` in `queries.ts`
|
||||
@@ -23,5 +23,5 @@ export async function revalidateTeamEventTypeCache({
|
||||
meetingSlug: string;
|
||||
orgSlug: string | null;
|
||||
}) {
|
||||
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`);
|
||||
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`, "max");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ShellMainAppDirBackButton } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDirBackButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
import type { LayoutProps } from "@calcom/features/shell/Shell";
|
||||
import type { LayoutProps } from "~/shell/Shell";
|
||||
|
||||
// Copied from `ShellMain` but with a different `ShellMainAppDirBackButton` import
|
||||
// for client/server component separation
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { LayoutProps } from "@calcom/features/shell/Shell";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
import type { LayoutProps } from "~/shell/Shell";
|
||||
|
||||
export const ShellMainAppDirBackButton = ({ backPath }: { backPath: LayoutProps["backPath"] }) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
|
||||
@@ -8,10 +8,10 @@ import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
|
||||
import { AvailabilitySliderTable } from "@calcom/web/modules/timezone-buddy/components/AvailabilitySliderTable";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -64,8 +64,8 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
||||
const organizationRepository = getOrganizationRepository();
|
||||
const isOrgPrivate = organizationId
|
||||
? await organizationRepository.checkIfPrivate({
|
||||
orgId: organizationId,
|
||||
})
|
||||
orgId: organizationId,
|
||||
})
|
||||
: false;
|
||||
|
||||
const permissionService = new PermissionCheckService();
|
||||
@@ -80,9 +80,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
||||
<ShellMainAppDir
|
||||
heading={t("availability")}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={
|
||||
<AvailabilityCTA canViewTeamAvailability={canViewTeamAvailability} />
|
||||
}>
|
||||
CTA={<AvailabilityCTA canViewTeamAvailability={canViewTeamAvailability} />}>
|
||||
{searchParams?.type === "team" && canViewTeamAvailability ? (
|
||||
<AvailabilitySliderTable isOrg={!!organizationId} />
|
||||
) : (
|
||||
|
||||
@@ -3,10 +3,11 @@ import { cookies, headers } from "next/headers";
|
||||
import React from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
const Layout = async ({ children }: { children: React.ReactNode }) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (session?.user?.id) SentrySetUser({ id: session.user.id });
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidateTeamsList() {
|
||||
revalidateTag("viewer.teams.list");
|
||||
revalidateTag("viewer.teams.list", "max");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { SearchParams } from "app/_types";
|
||||
import type { Session } from "next-auth";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
@@ -10,6 +9,8 @@ import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TeamsListing } from "~/ee/teams/components/TeamsListing";
|
||||
|
||||
import { TeamsCTA } from "./CTA";
|
||||
|
||||
const getCachedTeams = unstable_cache(
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import { TeamsCTA } from "app/(use-page-wrapper)/(main-nav)/teams/CTA";
|
||||
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
|
||||
export const TeamsListSkeleton = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
|
||||
@@ -162,14 +162,14 @@ function Field({
|
||||
<SelectField
|
||||
maxMenuHeight={200}
|
||||
styles={{
|
||||
singleValue: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
option: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
singleValue: (baseStyles) =>
|
||||
Object.assign({}, baseStyles, {
|
||||
fontSize: "14px",
|
||||
}),
|
||||
option: (baseStyles) =>
|
||||
Object.assign({}, baseStyles, {
|
||||
fontSize: "14px",
|
||||
}),
|
||||
}}
|
||||
label="Type"
|
||||
isDisabled={!!router}
|
||||
|
||||
@@ -8,9 +8,9 @@ import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-bu
|
||||
import type { JsonTree } from "react-awesome-query-builder";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Toaster } from "sonner";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils";
|
||||
import { buildEmptyQueryValue } from "@calcom/app-store/_utils/raqb/raqbUtils.client";
|
||||
import { raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils.server";
|
||||
import { routingFormAppComponents } from "@calcom/app-store/routing-forms/appComponents";
|
||||
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
|
||||
import { EmptyState } from "@calcom/app-store/routing-forms/components/_components/EmptyState";
|
||||
@@ -39,7 +39,6 @@ import type {
|
||||
EditFormRoute,
|
||||
AttributeRoutingConfig,
|
||||
} from "@calcom/app-store/routing-forms/types/types";
|
||||
import type { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer";
|
||||
@@ -83,32 +82,36 @@ function useEnsureEventTypeIdInRedirectUrlAction({
|
||||
eventOptions: { label: string; value: string; eventTypeId: number }[];
|
||||
setRoute: SetRoute;
|
||||
}) {
|
||||
const routeActionValue = isRouter(route) ? undefined : route.action.value;
|
||||
const routeActionType = isRouter(route) ? undefined : route.action.type;
|
||||
const routeActionEventTypeId = isRouter(route) ? undefined : route.action.eventTypeId;
|
||||
|
||||
useEffect(() => {
|
||||
if (isRouter(route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
route.action.type !== RouteActionType.EventTypeRedirectUrl ||
|
||||
routeActionType !== RouteActionType.EventTypeRedirectUrl ||
|
||||
// Must not be set already. Could be zero as well for custom
|
||||
route.action.eventTypeId !== undefined
|
||||
routeActionEventTypeId !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingOption = eventOptions.find((eventOption) => eventOption.value === route.action.value);
|
||||
const matchingOption = eventOptions.find((eventOption) => eventOption.value === routeActionValue);
|
||||
if (!matchingOption) {
|
||||
return;
|
||||
}
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, eventTypeId: matchingOption.eventTypeId },
|
||||
});
|
||||
}, [eventOptions, setRoute, route.id, (route as unknown as any).action?.value]);
|
||||
}, [eventOptions, setRoute, route, routeActionValue, routeActionType, routeActionEventTypeId]);
|
||||
}
|
||||
|
||||
const hasRules = (route: EditFormRoute) => {
|
||||
if (isRouter(route)) return false;
|
||||
route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
|
||||
return route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
|
||||
};
|
||||
|
||||
function getEmptyQueryValue() {
|
||||
@@ -147,14 +150,14 @@ const buildEventsData = ({
|
||||
label: string;
|
||||
value: string;
|
||||
eventTypeId: number;
|
||||
eventTypeAppMetadata?: Record<string, any>;
|
||||
eventTypeAppMetadata?: Record<string, unknown>;
|
||||
isRRWeightsEnabled: boolean;
|
||||
}[] = [];
|
||||
const eventTypesMap = new Map<
|
||||
number,
|
||||
{
|
||||
schedulingType: SchedulingType | null;
|
||||
eventTypeAppMetadata?: Record<string, any>;
|
||||
eventTypeAppMetadata?: Record<string, unknown>;
|
||||
}
|
||||
>();
|
||||
eventTypesByGroup?.eventTypeGroups.forEach((group) => {
|
||||
@@ -392,13 +395,11 @@ const Route = ({
|
||||
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
|
||||
: "";
|
||||
|
||||
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(() => {
|
||||
const isCustom =
|
||||
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
|
||||
setCustomEventTypeSlug(isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "");
|
||||
}, []);
|
||||
return isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "";
|
||||
});
|
||||
|
||||
useEnsureEventTypeIdInRedirectUrlAction({
|
||||
route,
|
||||
@@ -1147,67 +1148,6 @@ const Routes = ({
|
||||
hookForm,
|
||||
});
|
||||
|
||||
const { data: allForms } = trpc.viewer.appRoutingForms.forms.useQuery();
|
||||
|
||||
const notHaveAttributesQuery = ({ form }: { form: { routes: z.infer<typeof zodRoutes> } }) => {
|
||||
return form.routes?.every((route) => {
|
||||
if (isRouter(route)) {
|
||||
return true;
|
||||
}
|
||||
return !route.attributesQueryValue;
|
||||
});
|
||||
};
|
||||
|
||||
const availableRouters =
|
||||
allForms?.filtered
|
||||
.filter(({ form: router }) => {
|
||||
const routerValidInContext = areTheySiblingEntities({
|
||||
entity1: {
|
||||
teamId: router.teamId ?? null,
|
||||
// group doesn't have userId. The query ensures that it belongs to the user only, if teamId isn't set. So, I am manually setting it to the form userId
|
||||
userId: router.userId,
|
||||
},
|
||||
entity2: {
|
||||
teamId: hookForm.getValues().teamId ?? null,
|
||||
userId: hookForm.getValues().userId,
|
||||
},
|
||||
});
|
||||
return router.id !== hookForm.getValues().id && routerValidInContext;
|
||||
})
|
||||
// We don't want to support picking forms that have attributes query. We can consider it later.
|
||||
// This is mainly because the Router picker feature is pretty much not used and we don't want to complicate things
|
||||
.filter(({ form }) => {
|
||||
return notHaveAttributesQuery({ form: form });
|
||||
})
|
||||
.map(({ form: router }) => {
|
||||
return {
|
||||
value: router.id,
|
||||
label: router.name,
|
||||
name: router.name,
|
||||
description: router.description,
|
||||
isDisabled: false,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
// const isConnectedForm = (id: string) => form.connectedForms.map((f) => f.id).includes(id);
|
||||
|
||||
// const routers: any[] = [];
|
||||
/* Disable this feature for new forms till we get it fully working with Routing Form with Attributes. This isn't much used feature */
|
||||
// const routers = availableRouters.map((r) => {
|
||||
// // Reset disabled state
|
||||
// r.isDisabled = false;
|
||||
|
||||
// // Can't select a form as router that is already a connected form. It avoids cyclic dependency
|
||||
// if (isConnectedForm(r.value)) {
|
||||
// r.isDisabled = true;
|
||||
// }
|
||||
// // A route that's already used, can't be reselected
|
||||
// if (routes.find((route) => route.id === r.value)) {
|
||||
// r.isDisabled = true;
|
||||
// }
|
||||
// return r;
|
||||
// });
|
||||
|
||||
const createRoute = useCreateRoute({
|
||||
routes,
|
||||
setRoutes,
|
||||
|
||||
@@ -5,11 +5,12 @@ import { RoutingFormAuthGuard } from "./RoutingFormAuthGuard";
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
params: { pages: string[] };
|
||||
params: Promise<{ pages: string[] }>;
|
||||
}
|
||||
|
||||
export default async function Layout({ children, params }: LayoutProps) {
|
||||
const isPublic = params.pages?.[0] === "routing-link";
|
||||
const { pages } = await params;
|
||||
const isPublic = pages?.[0] === "routing-link";
|
||||
if (isPublic) {
|
||||
return <FormProvider>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { useHasPaidPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
import { FilterResults } from "@calcom/features/filters/components/FilterResults";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
@@ -39,6 +34,12 @@ import {
|
||||
FormActionsProvider,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
|
||||
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
import { ShellMain } from "~/shell/Shell";
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNewFormDialogState }) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
|
||||
@@ -3,10 +3,11 @@ import { redirect } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
import FormProvider from "../[...pages]/FormProvider";
|
||||
|
||||
export default async function Layout({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -47,6 +47,8 @@ const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
? "SAML (like Okta)"
|
||||
: "your original login method";
|
||||
return t("account_managed_by_identity_provider_error", { provider: providerName });
|
||||
} else if (error === "saml-idp-not-authoritative") {
|
||||
return t("saml_idp_not_authoritative_error");
|
||||
}
|
||||
return t("error_during_login") + (error ? ` Error code: ${error}` : "");
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
// cookie will be cleared in `/apps/web/middleware.ts`
|
||||
// cookie will be cleared in `/apps/web/proxy.ts`
|
||||
const h = await headers();
|
||||
const context = buildLegacyCtx(h, await cookies(), await params, await searchParams);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
export function AvailabilityScheduleSkeleton() {
|
||||
return (
|
||||
<Shell withoutMain={true}>
|
||||
@@ -106,7 +107,7 @@ export function AvailabilityScheduleSkeleton() {
|
||||
</div>
|
||||
|
||||
{/* Sidebar - only visible on desktop */}
|
||||
<div className="min-w-40 col-span-3 hidden stack-y-2 md:block lg:col-span-1 xl:w-[25%]">
|
||||
<div className="min-w-40 stack-y-2 col-span-3 hidden md:block lg:col-span-1 xl:w-[25%]">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<SkeletonText className="mb-1 h-4 w-24 text-sm" /> {/* Timezone */}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
export default function Error() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import {
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
SkeletonAvatar,
|
||||
} from "@calcom/ui/components/skeleton";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
export const EventTypeEditPageSkeleton = () => {
|
||||
const backButtonSkeleton = (
|
||||
<div className="bg-default sticky top-0 z-10 mb-0 flex items-center py-2 md:mb-9 md:mt-0">
|
||||
@@ -33,7 +34,7 @@ export const EventTypeEditPageSkeleton = () => {
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||
<div className="hidden xl:block">
|
||||
<div className="primary-navigation w-64 shrink-0">
|
||||
<div className="flex flex-col stack-y-1">
|
||||
<div className="stack-y-1 flex flex-col">
|
||||
{/* Tab navigation items */}
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div
|
||||
@@ -58,14 +59,14 @@ export const EventTypeEditPageSkeleton = () => {
|
||||
<SkeletonText className="h-8 w-full max-w-md" />
|
||||
</div>
|
||||
|
||||
<div className="mb-6 stack-y-6">
|
||||
<div className="stack-y-6 mb-6">
|
||||
{/* Form fields */}
|
||||
<div className="flex flex-col stack-y-2">
|
||||
<div className="stack-y-2 flex flex-col">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col stack-y-2">
|
||||
<div className="stack-y-2 flex flex-col">
|
||||
<SkeletonText className="h-4 w-40" />
|
||||
<div className="flex items-center">
|
||||
<SkeletonText className="h-10 w-full" />
|
||||
@@ -73,12 +74,12 @@ export const EventTypeEditPageSkeleton = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col stack-y-2">
|
||||
<div className="stack-y-2 flex flex-col">
|
||||
<SkeletonText className="h-4 w-36" />
|
||||
<SkeletonText className="h-24 w-full" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col stack-y-2">
|
||||
<div className="stack-y-2 flex flex-col">
|
||||
<SkeletonText className="h-4 w-28" />
|
||||
<div className="flex items-center">
|
||||
<SkeletonText className="h-10 w-full" />
|
||||
@@ -86,7 +87,7 @@ export const EventTypeEditPageSkeleton = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col stack-y-2">
|
||||
<div className="stack-y-2 flex flex-col">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
export default function UpgradeTipWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
import UpgradeTipWrapper from "./UpgradeTipWrapper";
|
||||
|
||||
@@ -9,14 +11,13 @@ export default async function InsightsLayout({ children }: { children: React.Rea
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
<Shell withoutMain={true}>
|
||||
<ShellMainAppDir
|
||||
heading={t("insights")}
|
||||
subtitle={t("insights_subtitle")}
|
||||
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
|
||||
actions={<div className={`flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
|
||||
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
|
||||
</Shell>
|
||||
</div>
|
||||
</ShellMainAppDir>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export default async function PageWrapperLayout({ children }: { children: React.
|
||||
key={script.id}
|
||||
nonce={nonce}
|
||||
id={script.id}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: script.script,
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -8,7 +8,6 @@ import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { TeamInviteEmailView } from "~/onboarding/teams/invite/email/team-invite-email-view";
|
||||
import { TeamInviteView } from "~/onboarding/teams/invite/team-invite-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
|
||||
@@ -30,7 +30,6 @@ const fetchReferralsToken = async () => {
|
||||
// The enabled referrals page implementation
|
||||
export const DubReferralsPage = () => {
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { t } = useLocale();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@@ -42,13 +41,11 @@ export const DubReferralsPage = () => {
|
||||
} catch (err) {
|
||||
console.error("Error fetching referrals token:", err);
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getToken();
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
if (!IS_DUB_REFERRALS_ENABLED || !token) {
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getTranslate } from "app/_utils";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { IS_DUB_REFERRALS_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
import { DubReferralsPage } from "./DubReferralsPage";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -4,10 +4,11 @@ import { usePathname } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React from "react";
|
||||
|
||||
import type Shell from "@calcom/features/shell/Shell";
|
||||
import type { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
|
||||
|
||||
import type Shell from "~/shell/Shell";
|
||||
|
||||
export type AdminLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
userRole: UserPermissionRole | "INACTIVE_ADMIN" | undefined;
|
||||
@@ -18,7 +19,7 @@ export default function AdminLayoutAppDirClient({ children }: AdminLayoutProps)
|
||||
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
|
||||
return (
|
||||
<div className="divide-subtle bg-default mx-auto flex max-w-4xl flex-row divide-y">
|
||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 *:flex-1"}>
|
||||
<div className={isAppsPage ? "min-w-0" : "*:flex-1 flex flex-1"}>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { z } from "zod";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { OrgForm } from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage";
|
||||
import { OrgForm } from "~/ee/organizations/admin/AdminOrgEditPage";
|
||||
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import AdminOrgTable from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
import AdminOrgTable from "~/ee/organizations/admin/AdminOrgPage";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import WorkspacePlatformsPage from "@calcom/features/ee/organizations/pages/settings/admin/WorkspacePlatformPage";
|
||||
import WorkspacePlatformsPage from "~/ee/organizations/admin/WorkspacePlatformPage";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
|
||||
+2
-1
@@ -13,7 +13,6 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
|
||||
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { TeamFeatures } from "@calcom/features/flags/config";
|
||||
import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
@@ -30,6 +29,8 @@ import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
|
||||
import { VerticalTabItem } from "@calcom/ui/components/navigation";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
const tabs: VerticalTabItemProps[] = [
|
||||
{
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidateApiKeysList() {
|
||||
revalidateTag("viewer.apiKeys.list");
|
||||
revalidateTag("viewer.apiKeys.list", "max");
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhooks-skeleton";
|
||||
import { SkeletonLoader } from "~/webhooks/views/webhooks-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonLoader />;
|
||||
|
||||
+2
-1
@@ -4,12 +4,13 @@ import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import WebhooksView from "~/webhooks/views/webhooks-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("webhooks"),
|
||||
|
||||
+2
-1
@@ -2,9 +2,10 @@ import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
|
||||
import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { EditWebhookView } from "~/webhooks/views/webhook-edit-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
(t) => t("webhooks"),
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhook-new-skeleton";
|
||||
import { SkeletonLoader } from "~/webhooks/views/webhook-new-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonLoader />;
|
||||
|
||||
+2
-1
@@ -1,11 +1,12 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
|
||||
|
||||
import { NewWebhookView } from "~/webhooks/views/webhook-new-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("webhooks"),
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import OrgAttributesEditPage from "@calcom/web/modules/ee/organizations/attributes/attributes-edit-view";
|
||||
|
||||
import { validateUserHasOrgPerms } from "../../../../actions/validateUserHasOrgPerms";
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
|
||||
import OrgAttributesCreatePage from "@calcom/web/modules/ee/organizations/attributes/attributes-create-view";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { validateUserHasOrgPerms } from "../../../actions/validateUserHasOrgPerms";
|
||||
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import OrgSettingsAttributesPage from "~/ee/organizations/attributes/attributes-list-view";
|
||||
|
||||
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
|
||||
import DelegationCredentialList from "~/ee/organizations/delegationCredential";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import GuestNotificationsView from "@calcom/features/ee/organizations/pages/settings/guest-notifications";
|
||||
import GuestNotificationsView from "~/ee/organizations/guest-notifications";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
|
||||
import PrivacyView from "~/ee/organizations/privacy";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { getTranslate, _generateMetadata } from "app/_utils";
|
||||
|
||||
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { AdminAPIView } from "~/ee/organizations/admin-api";
|
||||
|
||||
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
|
||||
import LegacyPage from "~/ee/organizations/general";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
|
||||
import LegacyPage from "~/ee/organizations/profile";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
-1
@@ -139,7 +139,6 @@ export function AdvancedPermissionGroup({
|
||||
}
|
||||
|
||||
const isChecked = selectedPermissions.includes(permission);
|
||||
const isReadPermission = action === CrudAction.Read;
|
||||
const isAutoEnabled = isReadAutoEnabled(action);
|
||||
|
||||
return (
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import { useState, useMemo, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import type { Resource, PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
CrudAction,
|
||||
Scope,
|
||||
@@ -160,7 +160,7 @@ export function RoleSheet({
|
||||
teamId,
|
||||
roleId: role.id,
|
||||
name: values.name,
|
||||
permissions: values.permissions as any,
|
||||
permissions: values.permissions as PermissionString[],
|
||||
color: values.color,
|
||||
});
|
||||
} else {
|
||||
@@ -169,7 +169,7 @@ export function RoleSheet({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
color: values.color,
|
||||
permissions: values.permissions as any,
|
||||
permissions: values.permissions as PermissionString[],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+7
-7
@@ -10,13 +10,13 @@ export async function revalidateTeamRoles(teamId: number) {
|
||||
revalidatePath("/settings/teams/[id]/roles", "page");
|
||||
|
||||
// Invalidate cache tags that match the unstable_cache keys
|
||||
revalidateTag("team-roles");
|
||||
revalidateTag("resource-permissions");
|
||||
revalidateTag("team-feature");
|
||||
revalidateTag("team-roles", "max");
|
||||
revalidateTag("resource-permissions", "max");
|
||||
revalidateTag("team-feature", "max");
|
||||
|
||||
// Also invalidate team-specific cache tags for completeness
|
||||
revalidateTag(`team-roles-${teamId}`);
|
||||
revalidateTag(`resource-permissions-${teamId}`);
|
||||
revalidateTag(`team-members-${teamId}`);
|
||||
revalidateTag(`team-features-${teamId}`);
|
||||
revalidateTag(`team-roles-${teamId}`, "max");
|
||||
revalidateTag(`resource-permissions-${teamId}`, "max");
|
||||
revalidateTag(`team-members-${teamId}`, "max");
|
||||
revalidateTag(`team-features-${teamId}`, "max");
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
|
||||
export function SkeletonLoader() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import LegacyPage from "~/ee/teams/views/team-appearance-view";
|
||||
|
||||
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage, {
|
||||
TeamMembersCTA,
|
||||
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
|
||||
} from "~/ee/organizations/other-team-members-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
|
||||
import LegacyPage from "~/ee/organizations/other-team-profile-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import LegacyPage from "~/ee/teams/views/team-appearance-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
(t) => t("booking_appearance"),
|
||||
|
||||
+1
-2
@@ -11,11 +11,10 @@ import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
import { TeamMembersView } from "@calcom/web/modules/ee/teams/views/team-members-view";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { TeamMembersView } from "~/teams/team-members-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
(t) => t("team_members"),
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage from "@calcom/features/ee/teams/pages/team-profile-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import LegacyPage from "@calcom/web/modules/ee/teams/views/team-profile-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import TeamSettingsView from "@calcom/features/ee/teams/pages/team-settings-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import TeamSettingsView from "@calcom/web/modules/ee/teams/views/team-settings-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/organizations/pages/settings/appearance";
|
||||
import Page from "~/ee/organizations/appearance";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const t = await getTranslate();
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
|
||||
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <UserListTableSkeleton />;
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidateAttributesList() {
|
||||
revalidateTag("viewer.attributes.list");
|
||||
revalidateTag("viewer.attributes.list", "max");
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
|
||||
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <UserListTableSkeleton />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/about-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/add-teams-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||
import LegacyPage, { LayoutWrapper } from "~/ee/organizations/new/onboarding-handover";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user