Merge branch 'main' into crypto-base64

This commit is contained in:
Peer Richelsen 2024-01-08 15:31:14 +00:00 committed by GitHub
commit d92d346c9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
239 changed files with 5823 additions and 1816 deletions

View File

@ -127,4 +127,12 @@ ZOHOCRM_CLIENT_ID=""
ZOHOCRM_CLIENT_SECRET=""
# - REVERT
# Used for the Pipedrive integration (via/ Revert (https://revert.dev))
# @see https://github.com/calcom/cal.com/#obtaining-revert-api-keys
REVERT_API_KEY=
REVERT_PUBLIC_TOKEN=
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
REVERT_API_URL=https://api.revert.dev/
# *********************************************************************************************************

View File

@ -133,6 +133,11 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Used for capturing exceptions and logging messages
NEXT_PUBLIC_SENTRY_DSN=
# Formbricks Experience Management Integration
FORMBRICKS_HOST_URL=https://app.formbricks.com
FORMBRICKS_ENVIRONMENT_ID=
FORMBRICKS_FEEDBACK_SURVEY_ID=
# Twilio
# Used to send SMS reminders in workflows
TWILIO_SID=
@ -322,3 +327,7 @@ APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1

View File

@ -19,12 +19,12 @@ Fixes # (issue)
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Tests (Unit/Integration/E2E or any other test)
- [ ] This change requires a documentation update
- Bug fix (non-breaking change which fixes an issue)
- Chore (refactoring code, technical debt, workflow improvements)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Tests (Unit/Integration/E2E or any other test)
- This change requires a documentation update
## How should this be tested?

View File

@ -24,7 +24,6 @@ runs:
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
DATABASE_URL: ${{ inputs.DATABASE_URL }}
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
if: steps.cache-db.outputs.cache-hit != 'true'
shell: bash

View File

@ -5,7 +5,7 @@ runs:
steps:
- name: Cache playwright binaries
id: playwright-cache
uses: buildjet/cache@v2
uses: buildjet/cache@v3
with:
path: |
~/Library/Caches/ms-playwright

View File

@ -1,74 +0,0 @@
name: "Apply issue labels to PR"
on:
pull_request_target:
types:
- opened
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
contents: none
issues: read
pull-requests: write
steps:
- name: Apply labels from linked issue to PR
uses: actions/github-script@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
async function getLinkedIssues(owner, repo, prNumber) {
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}`;
const variables = {
owner: owner,
repo: repo,
prNumber: prNumber,
};
const result = await github.graphql(query, variables);
return result.repository.pullRequest.closingIssuesReferences.nodes;
}
const pr = context.payload.pull_request;
const linkedIssues = await getLinkedIssues(
context.repo.owner,
context.repo.repo,
pr.number
);
const labelsToAdd = new Set();
for (const issue of linkedIssues) {
if (issue.labels && issue.labels.nodes) {
for (const label of issue.labels.nodes) {
labelsToAdd.add(label.name);
}
}
}
if (labelsToAdd.size) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: Array.from(labelsToAdd),
});
}

View File

@ -2,7 +2,7 @@ name: Check types
on:
workflow_call:
env:
NODE_OPTIONS: "--max-old-space-size=8192"
NODE_OPTIONS: --max-old-space-size=4096
jobs:
check-types:
runs-on: buildjet-4vcpu-ubuntu-2204

View File

@ -17,10 +17,11 @@ jobs:
steps:
- uses: actions/stale@v7
with:
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: 7
days-before-pr-close: -1
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
operations-per-run: 100

View File

@ -1,7 +1,8 @@
name: E2E App-Store Apps Tests
on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
e2e-app-store:
timeout-minutes: 20
@ -29,11 +30,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
env:
env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
@ -75,7 +75,7 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results

View File

@ -1,7 +1,8 @@
name: E2E Embed React tests and booking flow (for non-embed as well)
on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
e2e-embed:
timeout-minutes: 20
@ -24,7 +25,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
@ -61,7 +61,7 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results

View File

@ -1,7 +1,8 @@
name: E2E Embed Core tests and booking flow (for non-embed as well)
on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
e2e-embed:
timeout-minutes: 20
@ -29,7 +30,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
@ -65,7 +65,7 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results

View File

@ -1,8 +1,8 @@
name: E2E tests
on:
workflow_call:
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
e2e:
timeout-minutes: 20
@ -28,7 +28,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
@ -68,7 +67,7 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
path: test-results

View File

@ -1,6 +1,7 @@
name: "Pull Request Labeler"
on:
- pull_request_target
pull_request_target:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
@ -16,3 +17,81 @@ jobs:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
sync-labels: ""
team-labels:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: equitybee/team-label-action@main
with:
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
organization-name: calcom
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
apply-labels-from-issue:
runs-on: ubuntu-latest
permissions:
contents: none
issues: read
pull-requests: write
steps:
- name: Apply labels from linked issue to PR
uses: actions/github-script@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
async function getLinkedIssues(owner, repo, prNumber) {
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}`;
const variables = {
owner: owner,
repo: repo,
prNumber: prNumber,
};
const result = await github.graphql(query, variables);
return result.repository.pullRequest.closingIssuesReferences.nodes;
}
const pr = context.payload.pull_request;
const linkedIssues = await getLinkedIssues(
context.repo.owner,
context.repo.repo,
pr.number
);
const labelsToAdd = new Set();
for (const issue of linkedIssues) {
if (issue.labels && issue.labels.nodes) {
for (const label of issue.labels.nodes) {
labelsToAdd.add(label.name);
}
}
}
if (labelsToAdd.size) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: Array.from(labelsToAdd),
});
}

View File

@ -25,7 +25,7 @@ jobs:
- name: Upload ESLint report
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: lint-results
path: lint-results

View File

@ -27,7 +27,7 @@ jobs:
npx -p nextjs-bundle-analysis@0.5.0 report
- name: Upload bundle
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: bundle
path: apps/web/.next/analyze/__bundle_analysis.json

View File

@ -1,16 +0,0 @@
name: Assign PR team labels
on:
pull_request:
branches:
- main
jobs:
team-labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: equitybee/team-label-action@main
with:
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
organization-name: calcom
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"

View File

@ -4,9 +4,6 @@ on:
pull_request_target:
branches:
- main
paths-ignore:
- "**.md"
- ".github/CODEOWNERS"
merge_group:
workflow_dispatch:
@ -15,36 +12,97 @@ concurrency:
cancel-in-progress: true
jobs:
changes:
name: Detect changes
runs-on: buildjet-4vcpu-ubuntu-2204
permissions:
pull-requests: read
outputs:
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS)"
type-check:
name: Type check
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/check-types.yml
secrets: inherit
test:
name: Unit tests
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
lint:
name: Linters
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/lint.yml
secrets: inherit
build:
name: Production build
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
name: E2E tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e.yml
secrets: inherit
e2e-app-store:
name: E2E App Store tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-app-store.yml
secrets: inherit
e2e-embed:
name: E2E embeds tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed.yml
secrets: inherit
e2e-embed-react:
name: E2E React embeds tests
needs: [changes, lint, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/e2e-embed-react.yml
secrets: inherit
analyze:
needs: build
name: Analyze Build
needs: [changes, build]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/nextjs-bundle-analysis.yml
secrets: inherit
required:
needs: [lint, type-check, test, build]
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
run: exit 1

View File

@ -2,9 +2,6 @@ name: Pre-release checks
on:
workflow_dispatch:
push:
branches:
- main
jobs:
changes:

View File

@ -11,7 +11,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
- uses: ./.github/actions/yarn-install
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
- run: yarn test

View File

@ -554,6 +554,10 @@ following
[Follow these steps](./packages/app-store/zoho-bigin/)
### Obtaining Pipedrive Client ID and Secret
[Follow these steps](./packages/app-store/pipedrive-crm/)
## Workflows
### Setting up SendGrid for Email reminders

View File

@ -48,17 +48,21 @@ Here is the full architecture:
### Email Router
To expose the AI app, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool.
For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe))
For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first.
To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/)
2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication).
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`, both with priority `10` if prompted.
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted.
4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain.
5. In the Destination URL field, use the nGrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
6. Activate "POST the raw, full MIME message".
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the nGrok listener and server.
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
Please feel free to improve any part of this architecture!

View File

@ -17,6 +17,7 @@ const ROUTES: [URLPattern, boolean][] = [
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,

View File

@ -1,3 +1,14 @@
export type Params = {
[param: string]: string | string[] | undefined;
};
export type SearchParams = {
[param: string]: string | string[] | undefined;
};
export type PageProps = {
params: Params;
searchParams: SearchParams;
};
export type LayoutProps = { params: Params; children: React.ReactElement };

View File

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

View File

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

View File

@ -0,0 +1,70 @@
import LegacyPage from "@pages/getting-started/[[...step]]";
import { ssrInit } from "app/_trpc/ssrInit";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapperAppDir";
async function getData() {
const req = { headers: headers(), cookies: cookies() };
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
const ssr = await ssrInit();
await ssr.viewer.me.prefetch();
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
completedOnboarding: true,
teams: {
select: {
accepted: true,
team: {
select: {
id: true,
name: true,
logo: true,
},
},
},
},
},
});
if (!user) {
throw new Error("User from session not found");
}
if (user.completedOnboarding) {
redirect("/event-types");
}
return {
dehydratedState: await ssr.dehydrate(),
hasPendingInvites: user.teams.find((team: any) => team.accepted === false) ?? false,
};
}
export default async function Page() {
const props = await getData();
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<LegacyPage />
</PageWrapper>
);
}

View File

@ -1,20 +0,0 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

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

View File

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

View File

@ -1,22 +0,0 @@
// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder
import { headers } from "next/headers";
import { type ReactElement } from "react";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

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

View File

@ -1,10 +0,0 @@
import Page from "@pages/video/no-meeting-found";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "",
() => ""
);
export default Page;

View File

@ -1,21 +0,0 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

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

View File

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

View File

@ -1,14 +1,13 @@
import LegacyPage from "@pages/apps/categories/index";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `Categories | ${APP_NAME}`,
@ -43,14 +42,4 @@ async function getPageProps() {
};
}
export default async function Page() {
const props = await getPageProps();
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<LegacyPage {...props} />
</PageWrapper>
);
}
export default WithLayout({ getData: getPageProps, Page: LegacyPage, getLayout: null })<"P">;

View File

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

View File

@ -30,7 +30,7 @@ const getPageProps = async ({ params }: { params: Record<string, string | string
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
const { category } = await getPageProps({ params });
await getPageProps({ params });
return <LegacyPage />;
}

View File

@ -0,0 +1,81 @@
import AppsPage from "@pages/apps";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { APP_NAME } from "@calcom/lib/constants";
import type { AppCategories } from "@calcom/prisma/enums";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `Apps | ${APP_NAME}`,
() => ""
);
};
const getPageProps = async () => {
const ssr = await ssrInit();
const req = { headers: headers(), cookies: cookies() };
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
let appStore, userAdminTeams: UserAdminTeams;
if (session?.user?.id) {
userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
} else {
appStore = await getAppRegistry();
userAdminTeams = [];
}
const categoryQuery = appStore.map(({ categories }) => ({
categories: categories || [],
}));
const categories = categoryQuery.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
return {
categories: Object.entries(categories)
.map(([name, count]): { name: AppCategories; count: number } => ({
name: name as AppCategories,
count,
}))
.sort(function (a, b) {
return b.count - a.count;
}),
appStore,
userAdminTeams,
dehydratedState: await ssr.dehydrate(),
};
};
export default async function AppPageAppDir() {
const { categories, appStore, userAdminTeams, dehydratedState } = await getPageProps();
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper
getLayout={getLayout}
requiresLicense={false}
nonce={nonce}
themeBasis={null}
dehydratedState={dehydratedState}>
<AppsPage categories={categories} appStore={appStore} userAdminTeams={userAdminTeams} />
</PageWrapper>
);
}

View File

@ -1,6 +1,7 @@
import { ssgInit } from "app/_trpc/ssgInit";
import type { Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import type { ReactElement } from "react";
import { z } from "zod";
@ -8,8 +9,6 @@ import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import PageWrapper from "@components/PageWrapperAppDir";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const querySchema = z.object({
@ -43,14 +42,6 @@ const getData = async ({ params }: { params: Params }) => {
};
};
export default async function BookingPageLayout({ params, children }: Props) {
const props = await getData({ params });
return (
<PageWrapper requiresLicense={false} getLayout={getLayout} nonce={undefined} themeBasis={null} {...props}>
{children}
</PageWrapper>
);
}
export default WithLayout({ getLayout, getData })<"L">;
export const dynamic = "force-static";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,19 @@
import OldPage from "@pages/teams/index";
import { ssrInit } from "app/_trpc/ssrInit";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
type PageProps = {
params: Params;
};
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const ssr = await ssrInit();
await ssr.viewer.me.prefetch();
@ -41,24 +32,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
return { dehydratedState: await ssr.dehydrate() };
}
const Page = async ({ params }: PageProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const legacyCtx = buildLegacyCtx(h, cookies(), params);
// @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext`
const props = await getData(legacyCtx);
return (
<PageWrapper
getLayout={getLayout}
requiresLicense={false}
nonce={nonce}
themeBasis={null}
dehydratedState={props.dehydratedState}>
<OldPage />
</PageWrapper>
);
};
export default Page;
// @ts-expect-error getData arg
export default WithLayout({ getData, getLayout, Page: OldPage })<"P">;

View File

@ -1,30 +1,21 @@
import OldPage from "@pages/video/[uid]";
import { ssrInit } from "app/_trpc/ssrInit";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MarkdownIt from "markdown-it";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
() => `${APP_NAME} Video`,
(t) => t("quick_video_meeting")
);
type PageProps = Readonly<{
params: Params;
}>;
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
@ -107,24 +98,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
};
}
const Page = async ({ params }: PageProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
// @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext`
const { dehydratedState, ...restProps } = await getData(legacyCtx);
return (
<PageWrapper
getLayout={null}
requiresLicense={false}
nonce={nonce}
themeBasis={null}
dehydratedState={dehydratedState}>
<OldPage {...restProps} />
</PageWrapper>
);
};
export default Page;
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -1,26 +1,17 @@
import OldPage from "@pages/video/meeting-ended/[uid]";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Meeting Unavailable",
() => "Meeting Unavailable"
);
type PageProps = Readonly<{
params: Params;
}>;
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const booking = await prisma.booking.findUnique({
where: {
@ -58,19 +49,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
};
}
const Page = async ({ params }: PageProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
// @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext`
const props = await getData(legacyCtx);
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
<OldPage {...props} />
</PageWrapper>
);
};
export default Page;
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -1,16 +1,12 @@
import OldPage from "@pages/video/meeting-not-started/[uid]";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
type PageProps = Readonly<{
params: Params;
}>;
@ -51,19 +47,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
};
}
const Page = async ({ params }: PageProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
// @ts-expect-error `req` of type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to `req` in `GetServerSidePropsContext`
const props = await getData(legacyCtx);
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
<OldPage {...props} />
</PageWrapper>
);
};
export default Page;
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -0,0 +1,20 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { ssrInit } from "app/_trpc/ssrInit";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("no_meeting_found"),
(t) => t("no_meeting_found")
);
const getData = async () => {
const ssr = await ssrInit();
return {
dehydratedState: await ssr.dehydrate(),
};
};
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ export default function AdminLayout({
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
<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"}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>

View File

@ -384,7 +384,7 @@ function BookingListItem(booking: BookingItemProps) {
target="_blank"
title={locationToDisplay}
rel="noreferrer"
className="text-sm leading-6 text-blue-600 hover:underline">
className="text-sm leading-6 text-blue-600 hover:underline dark:text-blue-400">
<div className="flex items-center gap-2">
{provider?.iconUrl && (
<img

View File

@ -3,7 +3,6 @@ import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
type FormData = {
@ -108,9 +108,7 @@ const UserProfile = () => {
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<OrganizationMemberAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />
)}
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />}
<input
ref={avatarRef}
type="hidden"

View File

@ -5,8 +5,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { UserAvatar } from "@components/ui/avatar/UserAvatar";
import { UserAvatar } from "@calcom/ui";
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];

View File

@ -81,7 +81,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="me-2 ms-2 flex flex-row space-x-2">
<div className="relative bottom-[6px] me-2 ms-2 flex flex-row space-x-2">
<Button
type="button"
onClick={() => setOpenDialogSaveUsername(true)}
@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-3.5")}>
{usernameIsAvailable ? <Check className="h-4 w-4" /> : <></>}
{usernameIsAvailable ? <Check className="relative bottom-[6px] h-4 w-4" /> : <></>}
</span>
</div>
)}

View File

@ -1,19 +0,0 @@
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { User } from "@calcom/prisma/client";
import { Avatar } from "@calcom/ui";
type UserAvatarProps = Omit<React.ComponentProps<typeof Avatar>, "alt" | "imageSrc"> & {
user: Pick<User, "organizationId" | "name" | "username">;
/**
* Useful when allowing the user to upload their own avatar and showing the avatar before it's uploaded
*/
previewSrc?: string | null;
};
/**
* It is aware of the user's organization to correctly show the avatar from the correct URL
*/
export function UserAvatar(props: UserAvatarProps) {
const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props;
return <Avatar {...rest} alt={user.name || "Nameless User"} imageSrc={previewSrc} />;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,855 @@
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import type { Team, User } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import type { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const log = logger.getSubLogger({ prefix: ["orgMigration"] });
type UserMetadata = {
migratedToOrgFrom?: {
username: string;
reverted: boolean;
revertTime: string;
lastMigrationTime: string;
};
};
/**
* Make sure that the migration is idempotent
*/
export async function moveUserToOrg({
user: { id: userId, userName: userName },
targetOrg: {
id: targetOrgId,
username: targetOrgUsername,
membership: { role: targetOrgRole, accepted: targetOrgMembershipAccepted = true },
},
shouldMoveTeams,
}: {
user: { id?: number; userName?: string };
targetOrg: {
id: number;
username?: string;
membership: { role: MembershipRole; accepted?: boolean };
};
shouldMoveTeams: boolean;
}) {
assertUserIdOrUserName(userId, userName);
const team = await getTeamOrThrowError(targetOrgId);
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (!teamMetadata?.isOrganization) {
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
}
const targetOrganization = {
...team,
metadata: teamMetadata,
};
const userToMoveToOrg = await getUniqueUserThatDoesntBelongToOrg(userName, userId, targetOrgId);
assertUserPartOfOtherOrg(userToMoveToOrg, userName, userId, targetOrgId);
if (!targetOrgUsername) {
targetOrgUsername = getOrgUsernameFromEmail(
userToMoveToOrg.email,
targetOrganization.metadata.orgAutoAcceptEmail || ""
);
}
const userWithSameUsernameInOrg = await prisma.user.findFirst({
where: {
username: targetOrgUsername,
organizationId: targetOrgId,
},
});
log.debug({
userWithSameUsernameInOrg,
targetOrgUsername,
targetOrgId,
userId,
});
if (userWithSameUsernameInOrg && userWithSameUsernameInOrg.id !== userId) {
throw new HttpError({
statusCode: 400,
message: `Username ${targetOrgUsername} already exists for orgId: ${targetOrgId} for some other user`,
});
}
assertUserPartOfOrgAndRemigrationAllowed(userToMoveToOrg, targetOrgId, targetOrgUsername, userId);
const orgMetadata = teamMetadata;
const userToMoveToOrgMetadata = (userToMoveToOrg.metadata || {}) as UserMetadata;
const nonOrgUserName =
(userToMoveToOrgMetadata.migratedToOrgFrom?.username as string) || userToMoveToOrg.username;
if (!nonOrgUserName) {
throw new HttpError({
statusCode: 400,
message: `User with id: ${userId} doesn't have a non-org username`,
});
}
await dbMoveUserToOrg({ userToMoveToOrg, targetOrgId, targetOrgUsername, nonOrgUserName });
let teamsToBeMovedToOrg;
if (shouldMoveTeams) {
teamsToBeMovedToOrg = await moveTeamsWithoutMembersToOrg({ targetOrgId, userToMoveToOrg });
}
await updateMembership({ targetOrgId, userToMoveToOrg, targetOrgRole, targetOrgMembershipAccepted });
await addRedirect({
nonOrgUserName,
teamsToBeMovedToOrg: teamsToBeMovedToOrg || [],
organization: targetOrganization,
targetOrgUsername,
});
await setOrgSlugIfNotSet(targetOrganization, orgMetadata, targetOrgId);
log.debug(`orgId:${targetOrgId} attached to userId:${userId}`);
}
/**
* Make sure that the migration is idempotent
*/
export async function removeUserFromOrg({ targetOrgId, userId }: { targetOrgId: number; userId: number }) {
const userToRemoveFromOrg = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!userToRemoveFromOrg) {
throw new HttpError({
statusCode: 400,
message: `User with id: ${userId} not found`,
});
}
if (userToRemoveFromOrg.organizationId !== targetOrgId) {
throw new HttpError({
statusCode: 400,
message: `User with id: ${userId} is not part of orgId: ${targetOrgId}`,
});
}
const userToRemoveFromOrgMetadata = (userToRemoveFromOrg.metadata || {}) as {
migratedToOrgFrom?: {
username: string;
reverted: boolean;
revertTime: string;
lastMigrationTime: string;
};
};
if (!userToRemoveFromOrgMetadata.migratedToOrgFrom) {
throw new HttpError({
statusCode: 400,
message: `User with id: ${userId} wasn't migrated. So, there is nothing to revert`,
});
}
const nonOrgUserName = userToRemoveFromOrgMetadata.migratedToOrgFrom.username as string;
if (!nonOrgUserName) {
throw new HttpError({
statusCode: 500,
message: `User with id: ${userId} doesn't have a non-org username`,
});
}
const teamsToBeRemovedFromOrg = await removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg });
await dbRemoveUserFromOrg({ userToRemoveFromOrg, nonOrgUserName });
await removeUserAlongWithItsTeamsRedirects({ nonOrgUserName, teamsToBeRemovedFromOrg });
await removeMembership({ targetOrgId, userToRemoveFromOrg });
log.debug(`orgId:${targetOrgId} attached to userId:${userId}`);
}
/**
* Make sure that the migration is idempotent
*/
export async function moveTeamToOrg({
targetOrg,
teamId,
moveMembers,
}: {
targetOrg: { id: number; teamSlug: string };
teamId: number;
moveMembers?: boolean;
}) {
const possibleOrg = await getTeamOrThrowError(targetOrg.id);
const { oldTeamSlug, updatedTeam } = await dbMoveTeamToOrg({ teamId, targetOrg });
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
if (!teamMetadata?.isOrganization) {
throw new Error(`${targetOrg.id} is not an Org`);
}
const targetOrganization = possibleOrg;
const orgMetadata = teamMetadata;
await addTeamRedirect({
oldTeamSlug,
teamSlug: updatedTeam.slug,
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
});
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
if (moveMembers) {
for (const membership of updatedTeam.members) {
await moveUserToOrg({
user: {
id: membership.userId,
},
targetOrg: {
id: targetOrg.id,
membership: {
role: membership.role,
accepted: membership.accepted,
},
},
shouldMoveTeams: false,
});
}
}
log.debug(`Successfully moved team ${teamId} to org ${targetOrg.id}`);
}
/**
* Make sure that the migration is idempotent
*/
export async function removeTeamFromOrg({ targetOrgId, teamId }: { targetOrgId: number; teamId: number }) {
const removedTeam = await dbRemoveTeamFromOrg({ teamId });
await removeTeamRedirect(removedTeam.slug);
log.debug(`Successfully removed team ${teamId} from org ${targetOrgId}`);
}
async function dbMoveTeamToOrg({
teamId,
targetOrg,
}: {
teamId: number;
targetOrg: {
id: number;
teamSlug: string;
};
}) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
members: true,
},
});
if (!team) {
throw new HttpError({
statusCode: 400,
message: `Team with id: ${teamId} not found`,
});
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const oldTeamSlug = teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug;
const updatedTeam = await prisma.team.update({
where: {
id: teamId,
},
data: {
slug: targetOrg.teamSlug,
parentId: targetOrg.id,
metadata: {
...teamMetadata,
migratedToOrgFrom: {
teamSlug: team.slug,
lastMigrationTime: new Date().toISOString(),
},
},
},
include: {
members: true,
},
});
return { oldTeamSlug, updatedTeam };
}
async function getUniqueUserThatDoesntBelongToOrg(
userName: string | undefined,
userId: number | undefined,
excludeOrgId: number
) {
log.debug("getUniqueUserThatDoesntBelongToOrg", { userName, userId, excludeOrgId });
if (userName) {
const matchingUsers = await prisma.user.findMany({
where: {
username: userName,
},
});
const foundUsers = matchingUsers.filter(
(user) => user.organizationId === excludeOrgId || user.organizationId === null
);
if (foundUsers.length > 1) {
throw new Error(`More than one user found with username: ${userName}`);
}
return foundUsers[0];
} else {
return await prisma.user.findUnique({
where: {
id: userId,
},
});
}
}
async function setOrgSlugIfNotSet(
targetOrganization: {
slug: string | null;
},
orgMetadata: {
requestedSlug?: string | undefined;
},
targetOrgId: number
) {
if (targetOrganization.slug) {
return;
}
if (!orgMetadata.requestedSlug) {
throw new HttpError({
statusCode: 400,
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
});
}
await setOrgSlug({
targetOrgId,
targetSlug: orgMetadata.requestedSlug,
});
}
function assertUserPartOfOrgAndRemigrationAllowed(
userToMoveToOrg: {
organizationId: User["organizationId"];
},
targetOrgId: number,
targetOrgUsername: string,
userId: number | undefined
) {
if (userToMoveToOrg.organizationId) {
if (userToMoveToOrg.organizationId !== targetOrgId) {
throw new HttpError({
statusCode: 400,
message: `User ${targetOrgUsername} already exists for different Org with orgId: ${targetOrgId}`,
});
} else {
log.debug(`Redoing migration for userId: ${userId} to orgId:${targetOrgId}`);
}
}
}
async function getTeamOrThrowError(targetOrgId: number) {
const team = await prisma.team.findUnique({
where: {
id: targetOrgId,
},
});
if (!team) {
throw new HttpError({
statusCode: 400,
message: `Org with id: ${targetOrgId} not found`,
});
}
return team;
}
function assertUserPartOfOtherOrg(
userToMoveToOrg: {
organizationId: User["organizationId"];
} | null,
userName: string | undefined,
userId: number | undefined,
targetOrgId: number
): asserts userToMoveToOrg {
if (!userToMoveToOrg) {
throw new HttpError({
message: `User ${userName ? userName : `ID:${userId}`} is part of an org already`,
statusCode: 400,
});
}
if (userToMoveToOrg.organizationId && userToMoveToOrg.organizationId !== targetOrgId) {
throw new HttpError({
message: `User is already a part of different organization ID: ${userToMoveToOrg.organizationId}`,
statusCode: 400,
});
}
}
function assertUserIdOrUserName(userId: number | undefined, userName: string | undefined) {
if (!userId && !userName) {
throw new HttpError({ statusCode: 400, message: "userId or userName is required" });
}
if (userId && userName) {
throw new HttpError({ statusCode: 400, message: "Provide either userId or userName" });
}
}
async function addRedirect({
nonOrgUserName,
organization,
targetOrgUsername,
teamsToBeMovedToOrg,
}: {
nonOrgUserName: string | null;
organization: Team;
targetOrgUsername: string;
teamsToBeMovedToOrg: { slug: string | null }[];
}) {
if (!nonOrgUserName) {
return;
}
const orgSlug = organization.slug || (organization.metadata as { requestedSlug?: string })?.requestedSlug;
if (!orgSlug) {
log.debug("No slug for org. Not adding the redirect", safeStringify({ organization, nonOrgUserName }));
return;
}
// If the user had a username earlier, we need to redirect it to the new org username
const orgUrlPrefix = getOrgFullOrigin(orgSlug);
log.debug({
orgUrlPrefix,
nonOrgUserName,
targetOrgUsername,
});
await prisma.tempOrgRedirect.upsert({
where: {
from_type_fromOrgId: {
type: RedirectType.User,
from: nonOrgUserName,
fromOrgId: 0,
},
},
create: {
type: RedirectType.User,
from: nonOrgUserName,
fromOrgId: 0,
toUrl: `${orgUrlPrefix}/${targetOrgUsername}`,
},
update: {
toUrl: `${orgUrlPrefix}/${targetOrgUsername}`,
},
});
for (const [, team] of Object.entries(teamsToBeMovedToOrg)) {
if (!team.slug) {
log.debug("No slug for team. Not adding the redirect", safeStringify({ team }));
continue;
}
await prisma.tempOrgRedirect.upsert({
where: {
from_type_fromOrgId: {
type: RedirectType.Team,
from: team.slug,
fromOrgId: 0,
},
},
create: {
type: RedirectType.Team,
from: team.slug,
fromOrgId: 0,
toUrl: `${orgUrlPrefix}/team/${team.slug}`,
},
update: {
toUrl: `${orgUrlPrefix}/team/${team.slug}`,
},
});
}
}
async function addTeamRedirect({
oldTeamSlug,
teamSlug,
orgSlug,
}: {
oldTeamSlug: string | null;
teamSlug: string | null;
orgSlug: string | null;
}) {
if (!oldTeamSlug) {
throw new HttpError({
statusCode: 400,
message: "No oldSlug for team. Not adding the redirect",
});
}
if (!teamSlug) {
throw new HttpError({
statusCode: 400,
message: "No slug for team. Not adding the redirect",
});
}
if (!orgSlug) {
log.warn(`No slug for org. Not adding the redirect`);
return;
}
const orgUrlPrefix = getOrgFullOrigin(orgSlug);
await prisma.tempOrgRedirect.upsert({
where: {
from_type_fromOrgId: {
type: RedirectType.Team,
from: oldTeamSlug,
fromOrgId: 0,
},
},
create: {
type: RedirectType.Team,
from: oldTeamSlug,
fromOrgId: 0,
toUrl: `${orgUrlPrefix}/${teamSlug}`,
},
update: {
toUrl: `${orgUrlPrefix}/${teamSlug}`,
},
});
}
async function updateMembership({
targetOrgId,
userToMoveToOrg,
targetOrgRole,
targetOrgMembershipAccepted,
}: {
targetOrgId: number;
userToMoveToOrg: User;
targetOrgRole: MembershipRole;
targetOrgMembershipAccepted: boolean;
}) {
log.debug("updateMembership", { targetOrgId, userToMoveToOrg, targetOrgRole, targetOrgMembershipAccepted });
await prisma.membership.upsert({
where: {
userId_teamId: {
teamId: targetOrgId,
userId: userToMoveToOrg.id,
},
},
create: {
teamId: targetOrgId,
userId: userToMoveToOrg.id,
role: targetOrgRole,
accepted: targetOrgMembershipAccepted,
},
update: {
role: targetOrgRole,
accepted: targetOrgMembershipAccepted,
},
});
}
async function dbMoveUserToOrg({
userToMoveToOrg,
targetOrgId,
targetOrgUsername,
nonOrgUserName,
}: {
userToMoveToOrg: User;
targetOrgId: number;
targetOrgUsername: string;
nonOrgUserName: string | null;
}) {
await prisma.user.update({
where: {
id: userToMoveToOrg.id,
},
data: {
organizationId: targetOrgId,
username: targetOrgUsername,
metadata: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...(userToMoveToOrg.metadata || {}),
migratedToOrgFrom: {
username: nonOrgUserName,
lastMigrationTime: new Date().toISOString(),
},
},
},
});
}
async function moveTeamsWithoutMembersToOrg({
targetOrgId,
userToMoveToOrg,
}: {
targetOrgId: number;
userToMoveToOrg: User;
}) {
const memberships = await prisma.membership.findMany({
where: {
userId: userToMoveToOrg.id,
},
});
const membershipTeamIds = memberships.map((m) => m.teamId);
const teams = await prisma.team.findMany({
where: {
id: {
in: membershipTeamIds,
},
},
select: {
id: true,
slug: true,
metadata: true,
},
});
const teamsToBeMovedToOrg = teams
.map((team) => {
return {
...team,
metadata: teamMetadataSchema.parse(team.metadata),
};
})
// Remove Orgs from the list
.filter((team) => !team.metadata?.isOrganization);
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
if (memberships.length) {
// Add the user's teams to the org
await prisma.team.updateMany({
where: {
id: {
in: teamIdsToBeMovedToOrg,
},
},
data: {
parentId: targetOrgId,
},
});
}
return teamsToBeMovedToOrg;
}
/**
* Make sure you pass it an organization ID only and not a team ID.
*/
async function setOrgSlug({ targetOrgId, targetSlug }: { targetOrgId: number; targetSlug: string }) {
await prisma.team.update({
where: {
id: targetOrgId,
},
data: {
slug: targetSlug,
},
});
}
async function removeTeamRedirect(teamSlug: string | null) {
if (!teamSlug) {
throw new HttpError({
statusCode: 400,
message: "No slug for team. Not removing the redirect",
});
return;
}
await prisma.tempOrgRedirect.deleteMany({
where: {
type: RedirectType.Team,
from: teamSlug,
fromOrgId: 0,
},
});
}
async function removeUserAlongWithItsTeamsRedirects({
nonOrgUserName,
teamsToBeRemovedFromOrg,
}: {
nonOrgUserName: string | null;
teamsToBeRemovedFromOrg: { slug: string | null }[];
}) {
if (!nonOrgUserName) {
return;
}
await prisma.tempOrgRedirect.deleteMany({
// This where clause is unique, so we will get only one result but using deleteMany because it doesn't throw an error if there are no rows to delete
where: {
type: RedirectType.User,
from: nonOrgUserName,
fromOrgId: 0,
},
});
for (const [, team] of Object.entries(teamsToBeRemovedFromOrg)) {
if (!team.slug) {
log.debug("No slug for team. Not removing the redirect", safeStringify({ team }));
continue;
}
await prisma.tempOrgRedirect.deleteMany({
where: {
type: RedirectType.Team,
from: team.slug,
fromOrgId: 0,
},
});
}
}
async function dbRemoveTeamFromOrg({ teamId }: { teamId: number }) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
});
if (!team) {
throw new HttpError({
statusCode: 400,
message: `Team with id: ${teamId} not found`,
});
}
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
try {
return await prisma.team.update({
where: {
id: teamId,
},
data: {
parentId: null,
slug: teamMetadata?.migratedToOrgFrom?.teamSlug || team.slug,
metadata: {
...teamMetadata,
migratedToOrgFrom: {
reverted: true,
lastRevertTime: new Date().toISOString(),
},
},
},
select: {
slug: true,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
throw new HttpError({
message: `Looks like the team's name is already taken by some other team outside the org or an org itself. Please change this team's name or the other team/org's name. If you rename the team that you are trying to remove from the org, you will have to manually remove the redirect from the database for that team as the slug would have changed.`,
statusCode: 400,
});
}
}
throw e;
}
}
async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { userToRemoveFromOrg: User }) {
const memberships = await prisma.membership.findMany({
where: {
userId: userToRemoveFromOrg.id,
},
});
const membershipTeamIds = memberships.map((m) => m.teamId);
const teams = await prisma.team.findMany({
where: {
id: {
in: membershipTeamIds,
},
},
select: {
id: true,
slug: true,
metadata: true,
},
});
const teamsToBeRemovedFromOrg = teams
.map((team) => {
return {
...team,
metadata: teamMetadataSchema.parse(team.metadata),
};
})
// Remove Orgs from the list
.filter((team) => !team.metadata?.isOrganization);
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
if (memberships.length) {
// Remove the user's teams from the org
await prisma.team.updateMany({
where: {
id: {
in: teamIdsToBeRemovedFromOrg,
},
},
data: {
parentId: null,
},
});
}
return teamsToBeRemovedFromOrg;
}
async function dbRemoveUserFromOrg({
userToRemoveFromOrg,
nonOrgUserName,
}: {
userToRemoveFromOrg: User;
nonOrgUserName: string;
}) {
await prisma.user.update({
where: {
id: userToRemoveFromOrg.id,
},
data: {
organizationId: null,
username: nonOrgUserName,
metadata: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...(userToRemoveFromOrg.metadata || {}),
migratedToOrgFrom: {
username: null,
reverted: true,
revertTime: new Date().toISOString(),
},
},
},
});
}
async function removeMembership({
targetOrgId,
userToRemoveFromOrg,
}: {
targetOrgId: number;
userToRemoveFromOrg: User;
}) {
await prisma.membership.deleteMany({
where: {
teamId: targetOrgId,
userId: userToRemoveFromOrg.id,
},
});
}

View File

@ -2,7 +2,7 @@ import type { GetServerSideProps } from "next";
import { csp } from "@lib/csp";
export type WithNonceProps<T extends Record<string, any>> = T & {
export type WithNonceProps<T extends Record<string, unknown>> = T & {
nonce?: string;
};
@ -11,7 +11,7 @@ export type WithNonceProps<T extends Record<string, any>> = T & {
* Note that if the Components are not adding any script tag then this is not needed. Even in absence of this, Document.getInitialProps would be able to generate nonce itself which it needs to add script tags common to all pages
* There is no harm in wrapping a `getServerSideProps` fn with this even if it doesn't add any script tag.
*/
export default function withNonce<T extends Record<string, any>>(
export default function withNonce<T extends Record<string, unknown>>(
getServerSideProps: GetServerSideProps<T>
): GetServerSideProps<WithNonceProps<T>> {
return async (context) => {

View File

@ -142,6 +142,8 @@ export const config = {
"/future/bookings/:status/",
"/video/:path*",
"/future/video/:path*",
"/teams",
"/future/teams/",
],
};

View File

@ -572,6 +572,8 @@ if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
nextConfig["sentry"] = {
autoInstrumentServerFunctions: true,
hideSourceMaps: true,
// disable source map generation for the server code
disableServerWebpackPlugin: !!process.env.SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN,
};
plugins.push(withSentryConfig);

View File

@ -39,7 +39,7 @@
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.37.0",
"@formkit/auto-animate": "^1.0.0-beta.5",
"@formkit/auto-animate": "^0.8.1",
"@glidejs/glide": "^3.5.2",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",

View File

@ -85,7 +85,7 @@ export default function Custom404() {
const isSuccessPage = pathname?.startsWith("/booking");
const isSubpage = pathname?.includes("/", 2) || isSuccessPage;
const isSignup = pathname?.startsWith("/signup");
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
@ -112,7 +112,7 @@ export default function Custom404() {
</div>
<div className="mt-12">
<div className="mt-8">
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
<Link href={WEBSITE_URL} className="text-base font-medium text-black hover:text-gray-500">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
@ -129,7 +129,7 @@ export default function Custom404() {
return (
<>
<HeadSeo
title={isSignup ? t("signup_requires") : t("404_page_not_found")}
title={t("404_page_not_found")}
description={t("404_page_not_found")}
nextSeoProps={{
nofollow: true,
@ -138,241 +138,130 @@ export default function Custom404() {
/>
<div className="bg-default min-h-screen px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
{isSignup && process.env.NEXT_PUBLIC_WEBAPP_URL !== "https://app.cal.com" ? (
<div>
<div>
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
{t("missing_license")}
</p>
<h1 className="font-cal text-emphasis mt-2 text-3xl font-extrabold">
{t("signup_requires")}
</h1>
<p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div>
<div className="mt-12">
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("next_steps")}
</h2>
<ul role="list" className="mt-4">
<li className="border-2 border-green-500 px-4 py-2">
<a
href="https://console.cal.com"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("acquire_commercial_license")}
</span>
</span>
</h3>
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<ul role="list" className="border-subtle divide-subtle divide-y">
<li className="px-4 py-2">
<Link
href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<FileText className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<div className="text-center">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{t("error_404")}</p>
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
{isSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
</h1>
{isSubpage && currentPageType !== pageType.TEAM ? (
<span className="mt-2 inline-block text-lg ">{t("check_spelling_mistakes_or_go_back")}</span>
) : IS_CALCOM ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
<strong className="text-blue-500">{username}</strong> {t("is_still_available")}{" "}
<span className="text-blue-500">{t("register_now")}</span>.
</a>
) : (
<span className="mt-2 inline-block text-lg">
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
{t("is_still_available")}
</span>
)}
</div>
<div className="mt-12">
{((!isSubpage && IS_CALCOM) ||
currentPageType === pageType.ORG ||
currentPageType === pageType.TEAM) && (
<ul role="list" className="my-4">
<li className="border-2 border-green-500 px-4 py-2">
<a
href={url}
target="_blank"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"
rel="noreferrer">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("prisma_studio_tip")}
{t("register")}{" "}
<strong className="text-green-500">{`${
currentPageType === pageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
}${username}${
currentPageType === pageType.ORG ? `.${subdomainSuffix()}` : ""
}`}</strong>
</span>
</h3>
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</li>
<li className="px-4 py-2">
<a
href={JOIN_DISCORD}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
Discord
</span>
</h3>
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<div className="mt-8">
<Link
href={`${WEBSITE_URL}/enterprise`}
className="hover:text-subtle text-emphasis text-base font-medium">
{t("contact_sales")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</div>
) : (
<>
<div className="text-center">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
{t("error_404")}
</p>
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
{isSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
</h1>
{isSubpage && currentPageType !== pageType.TEAM ? (
<span className="mt-2 inline-block text-lg ">
{t("check_spelling_mistakes_or_go_back")}
</span>
) : IS_CALCOM ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
<strong className="text-blue-500">{username}</strong> {t("is_still_available")}{" "}
<span className="text-blue-500">{t("register_now")}</span>.
</h3>
<p className="text-subtle text-base">
{t(`404_claim_entity_${currentPageType.toLowerCase()}`)}
</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
) : (
<span className="mt-2 inline-block text-lg">
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
{t("is_still_available")}
</span>
)}
</div>
<div className="mt-12">
{((!isSubpage && IS_CALCOM) ||
currentPageType === pageType.ORG ||
currentPageType === pageType.TEAM) && (
<ul role="list" className="my-4">
<li className="border-2 border-green-500 px-4 py-2">
<a
href={url}
target="_blank"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"
rel="noreferrer">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("register")}{" "}
<strong className="text-green-500">{`${
currentPageType === pageType.TEAM
? `${new URL(WEBSITE_URL).host}/team/`
: ""
}${username}${
currentPageType === pageType.ORG ? `.${subdomainSuffix()}` : ""
}`}</strong>
</span>
</span>
</h3>
<p className="text-subtle text-base">
{t(`404_claim_entity_${currentPageType.toLowerCase()}`)}
</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
)}
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("popular_pages")}
</h2>
<ul role="list" className="border-subtle divide-subtle divide-y">
{links
.filter((_, idx) => currentPageType === pageType.ORG || idx !== 0)
.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2">
<a
href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<link.icon className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{link.title}
</span>
</h3>
<p className="text-subtle text-base">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
))}
<li className="px-4 py-2">
</li>
</ul>
)}
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("popular_pages")}
</h2>
<ul role="list" className="border-subtle divide-subtle divide-y">
{links
.filter((_, idx) => currentPageType === pageType.ORG || idx !== 0)
.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2">
<a
href={JOIN_DISCORD}
href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<Discord className="text-default h-6 w-6" />
<link.icon className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
Discord
{link.title}
</span>
</h3>
<p className="text-subtle text-base">{t("join_our_community")}</p>
<p className="text-subtle text-base">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<div className="mt-8">
<Link href="/" className="hover:text-subtle text-emphasis text-base font-medium">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</>
)}
))}
<li className="px-4 py-2">
<a
href={JOIN_DISCORD}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<Discord className="text-default h-6 w-6" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
Discord
</span>
</h3>
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<div className="mt-8">
<Link href={WEBSITE_URL} className="hover:text-subtle text-emphasis text-base font-medium">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</main>
</div>
</>

View File

@ -11,7 +11,6 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
@ -28,6 +27,7 @@ import { RedirectType, type EventType, type User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { EmbedProps } from "@lib/withEmbedSsr";
@ -101,7 +101,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<OrganizationMemberAvatar
<UserAvatar
size="xl"
user={{
organizationId: profile.organization?.id,

View File

@ -28,6 +28,7 @@ MyApp.getInitialProps = async (ctx: AppContextType) => {
if (req) {
const { getLocale } = await import("@calcom/features/auth/lib/getLocale");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newLocale = await getLocale(req as IncomingMessage & { cookies: Record<string, any> });
} else if (typeof window !== "undefined" && window.calNewLocale) {
newLocale = window.calNewLocale;

View File

@ -32,7 +32,8 @@ class MyDocument extends Document<Props> {
const newLocale =
ctx.req && getLocaleModule
? await getLocaleModule.getLocale(ctx.req as IncomingMessage & { cookies: Record<string, any> })
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
await getLocaleModule.getLocale(ctx.req as IncomingMessage & { cookies: Record<string, any> })
: "en";
const asPath = ctx.asPath || "";
@ -87,7 +88,7 @@ class MyDocument extends Document<Props> {
</Head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
className="dark:bg-darkgray-50 todesktop:!bg-transparent bg-subtle antialiased"
style={
isEmbed
? {

View File

@ -3,9 +3,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const requriedScopes = ["READ_PROFILE"];
const requiredScopes = ["READ_PROFILE"];
const account = await isAuthorized(req, requriedScopes);
const account = await isAuthorized(req, requiredScopes);
if (!account) {
return res.status(401).json({ message: "Unauthorized" });

View File

@ -0,0 +1,76 @@
import { getFormSchema } from "@pages/settings/admin/orgMigrations/moveTeamToOrg";
import type { NextApiRequest, NextApiResponse } from "next/types";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { moveTeamToOrg } from "../../../lib/orgMigration";
const log = logger.getSubLogger({ prefix: ["moveTeamToOrg"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const rawBody = req.body;
log.debug(
"Moving team to org:",
safeStringify({
body: rawBody,
})
);
const translate = await getTranslation("en", "common");
const moveTeamToOrgSchema = getFormSchema(translate);
const parsedBody = moveTeamToOrgSchema.safeParse(rawBody);
const session = await getServerSession({ req, res });
if (!session) {
return res.status(403).json({ message: "No session found" });
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!parsedBody.success) {
log.error("moveTeamToOrg failed:", safeStringify(parsedBody.error));
return res.status(400).json({ message: JSON.stringify(parsedBody.error) });
}
const { teamId, targetOrgId, moveMembers, teamSlugInOrganization } = parsedBody.data;
const isAllowed = isAdmin;
if (!isAllowed) {
return res.status(403).json({ message: "Not Authorized" });
}
try {
await moveTeamToOrg({
targetOrg: {
id: targetOrgId,
teamSlug: teamSlugInOrganization,
},
teamId,
moveMembers,
});
} catch (error) {
if (error instanceof HttpError) {
if (error.statusCode > 300) {
log.error("moveTeamToOrg failed:", safeStringify(error.message));
}
return res.status(error.statusCode).json({ message: error.message });
}
log.error("moveTeamToOrg failed:", safeStringify(error));
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
return res.status(500).json({ message: errorMessage });
}
return res.status(200).json({
message: `Added team ${teamId} to Org: ${targetOrgId} ${
moveMembers ? " along with the members" : " without the members"
}`,
});
}

View File

@ -0,0 +1,75 @@
import { getFormSchema } from "@pages/settings/admin/orgMigrations/moveUserToOrg";
import type { NextApiRequest, NextApiResponse } from "next/types";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { moveUserToOrg } from "../../../lib/orgMigration";
const log = logger.getSubLogger({ prefix: ["moveUserToOrg"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const rawBody = req.body;
const translate = await getTranslation("en", "common");
const migrateBodySchema = getFormSchema(translate);
log.debug(
"Starting migration:",
safeStringify({
body: rawBody,
})
);
const parsedBody = migrateBodySchema.safeParse(rawBody);
const session = await getServerSession({ req });
if (!session) {
res.status(403).json({ message: "No session found" });
return;
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (parsedBody.success) {
const { userId, userName, shouldMoveTeams, targetOrgId, targetOrgUsername, targetOrgRole } =
parsedBody.data;
const isAllowed = isAdmin;
if (isAllowed) {
try {
await moveUserToOrg({
targetOrg: {
id: targetOrgId,
username: targetOrgUsername,
membership: {
role: targetOrgRole,
},
},
user: {
id: userId,
userName,
},
shouldMoveTeams,
});
} catch (error) {
if (error instanceof HttpError) {
if (error.statusCode > 300) {
log.error("Migration failed:", safeStringify(error));
}
return res.status(error.statusCode).json({ message: error.message });
}
log.error("Migration failed:", safeStringify(error));
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
return res.status(400).json({ message: errorMessage });
}
} else {
return res.status(403).json({ message: "Not Authorized" });
}
return res.status(200).json({ message: "Migrated" });
}
log.error("Migration failed:", safeStringify(parsedBody.error));
return res.status(400).json({ message: JSON.stringify(parsedBody.error) });
}

View File

@ -0,0 +1,63 @@
import { getFormSchema } from "@pages/settings/admin/orgMigrations/removeTeamFromOrg";
import type { NextApiRequest, NextApiResponse } from "next/types";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { removeTeamFromOrg } from "../../../lib/orgMigration";
const log = logger.getSubLogger({ prefix: ["removeTeamFromOrg"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const rawBody = req.body;
const translate = await getTranslation("en", "common");
const removeTeamFromOrgSchema = getFormSchema(translate);
log.debug(
"Removing team from org:",
safeStringify({
body: rawBody,
})
);
const parsedBody = removeTeamFromOrgSchema.safeParse(rawBody);
const session = await getServerSession({ req });
if (!session) {
return res.status(403).json({ message: "No session found" });
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!parsedBody.success) {
log.error("RemoveTeamFromOrg failed:", safeStringify(parsedBody.error));
return res.status(400).json({ message: JSON.stringify(parsedBody.error) });
}
const { teamId, targetOrgId } = parsedBody.data;
const isAllowed = isAdmin;
if (!isAllowed) {
return res.status(403).json({ message: "Not Authorized" });
}
try {
await removeTeamFromOrg({
targetOrgId,
teamId,
});
} catch (error) {
if (error instanceof HttpError) {
if (error.statusCode > 300) {
log.error("RemoveTeamFromOrg failed:", safeStringify(error));
}
return res.status(error.statusCode).json({ message: error.message });
}
log.error("RemoveTeamFromOrg failed:", safeStringify(error));
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
return res.status(500).json({ message: errorMessage });
}
return res.status(200).json({ message: `Removed team ${teamId} from ${targetOrgId}` });
}

View File

@ -0,0 +1,59 @@
import { getFormSchema } from "@pages/settings/admin/orgMigrations/removeUserFromOrg";
import type { NextApiRequest, NextApiResponse } from "next/types";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { removeUserFromOrg } from "../../../lib/orgMigration";
const log = logger.getSubLogger({ prefix: ["removeUserFromOrg"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const body = req.body;
log.debug(
"Starting reverse migration:",
safeStringify({
body,
})
);
const translate = await getTranslation("en", "common");
const migrateRevertBodySchema = getFormSchema(translate);
const parsedBody = migrateRevertBodySchema.safeParse(body);
const session = await getServerSession({ req });
if (!session) {
return res.status(403).json({ message: "No session found" });
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return res.status(403).json({ message: "Only admin can take this action" });
}
if (parsedBody.success) {
const { userId, targetOrgId } = parsedBody.data;
try {
await removeUserFromOrg({ targetOrgId, userId });
} catch (error) {
if (error instanceof HttpError) {
if (error.statusCode > 300) {
log.error("Reverse migration failed:", safeStringify(error));
}
return res.status(error.statusCode).json({ message: error.message });
}
log.error("Reverse migration failed:", safeStringify(error));
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
return res.status(500).json({ message: errorMessage });
}
return res.status(200).json({ message: "Reverted" });
}
log.error("Reverse Migration failed:", safeStringify(parsedBody.error));
return res.status(400).json({ message: JSON.stringify(parsedBody.error) });
}

View File

@ -21,16 +21,16 @@ const schema = z
id: z.string(),
payload: z.object({
recording_id: z.string(),
end_ts: z.number(),
end_ts: z.number().optional(),
room_name: z.string(),
start_ts: z.number(),
start_ts: z.number().optional(),
status: z.string(),
max_participants: z.number(),
duration: z.number(),
s3_key: z.string(),
max_participants: z.number().optional(),
duration: z.number().optional(),
s3_key: z.string().optional(),
}),
event_ts: z.number(),
event_ts: z.number().optional(),
})
.passthrough();

View File

@ -1,3 +1,5 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import type { ChangeEventHandler } from "react";
import { useState } from "react";
@ -64,7 +66,7 @@ export default function Apps({
categories,
appStore,
userAdminTeams,
}: inferSSRProps<typeof getServerSideProps>) {
}: Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">) {
const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";

View File

@ -15,7 +15,7 @@ function VerifyEmailPage() {
const { data } = useEmailVerifyCheck();
const { data: session } = useSession();
const router = useRouter();
const { t } = useLocale();
const { t, isLocaleReady } = useLocale();
const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
useEffect(() => {
@ -24,7 +24,9 @@ function VerifyEmailPage() {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.isVerified]);
if (!isLocaleReady) {
return null;
}
return (
<div className="h-[100vh] w-full ">
<div className="flex h-full w-full flex-col items-center justify-center">

View File

@ -486,48 +486,24 @@ export default function Success(props: SuccessProps) {
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3" data-testid="where">
{!rescheduleLocation || locationToDisplay === rescheduleLocationToDisplay ? (
locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className="text-default flex items-center gap-2"
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
locationToDisplay
)
<DisplayLocation
locationToDisplay={locationToDisplay}
providerName={providerName}
/>
) : (
<>
{!!formerTime &&
(locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className="text-default flex items-center gap-2 line-through"
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
<p className="line-through">{locationToDisplay}</p>
))}
{rescheduleLocationToDisplay.startsWith("http") ? (
<a
href={rescheduleLocationToDisplay}
target="_blank"
title={rescheduleLocationToDisplay}
className="text-default flex items-center gap-2"
rel="noreferrer">
{rescheduleProviderName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
rescheduleLocationToDisplay
{!!formerTime && (
<DisplayLocation
locationToDisplay={locationToDisplay}
providerName={providerName}
className="line-through"
/>
)}
<DisplayLocation
locationToDisplay={rescheduleLocationToDisplay}
providerName={rescheduleProviderName}
/>
</>
)}
</div>
@ -830,6 +806,29 @@ export default function Success(props: SuccessProps) {
);
}
const DisplayLocation = ({
locationToDisplay,
providerName,
className,
}: {
locationToDisplay: string;
providerName?: string;
className?: string;
}) =>
locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
target="_blank"
title={locationToDisplay}
className={classNames("text-default flex items-center gap-2", className)}
rel="noreferrer">
{providerName || "Link"}
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
<p className={className}>{locationToDisplay}</p>
);
Success.isBookingPage = true;
Success.PageWrapper = PageWrapper;

View File

@ -522,6 +522,32 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
const slug = formMethods.watch("slug") ?? eventType.slug;
// Optional prerender all tabs after 300 ms on mount
useEffect(() => {
const timeout = setTimeout(() => {
const Components = [
EventSetupTab,
EventAvailabilityTab,
EventTeamTab,
EventLimitsTab,
EventAdvancedTab,
EventInstantTab,
EventRecurringTab,
EventAppsTab,
EventWorkflowsTab,
EventWebhooksTab,
];
Components.forEach((C) => {
// @ts-expect-error Property 'render' does not exist on type 'ComponentClass
C.render.preload();
});
}, 300);
return () => {
clearTimeout(timeout);
};
}, []);
return (
<>
<EventTypeSingleLayout

View File

@ -19,6 +19,7 @@ import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { CAL_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
@ -66,6 +67,7 @@ import {
Trash,
Upload,
Users,
VenetianMask,
} from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
@ -388,6 +390,8 @@ export const EventTypeList = ({
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${bookerUrl}/${embedLink}`;
const isPrivateURLEnabled = type.hashedLink?.link;
const placeholderHashedLink = `${CAL_URL}/d/${type.hashedLink?.link}/${type.slug}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
@ -465,6 +469,20 @@ export const EventTypeList = ({
}}
/>
</Tooltip>
{isPrivateURLEnabled && (
<Tooltip content={t("copy_link")}>
<Button
color="secondary"
variant="icon"
StartIcon={VenetianMask}
onClick={() => {
showToast(t("private_link_copied"), "success");
navigator.clipboard.writeText(placeholderHashedLink);
}}
/>
</Tooltip>
)}
</>
)}
<Dropdown modal={false}>
@ -907,6 +925,7 @@ const EventTypesPage = () => {
const searchParams = useCompatSearchParams();
const { open } = useIntercom();
const { data: user } = useMeQuery();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [showProfileBanner, setShowProfileBanner] = useState(false);
const orgBranding = useOrgBranding();
const routerQuery = useRouterQuery();
@ -919,12 +938,6 @@ const EventTypesPage = () => {
staleTime: 1 * 60 * 60 * 1000,
});
function closeBanner() {
setShowProfileBanner(false);
document.cookie = `calcom-profile-banner=1;max-age=${60 * 60 * 24 * 90}`; // 3 months
showToast(t("we_wont_show_again"), "success");
}
useEffect(() => {
if (searchParams?.get("openIntercom") === "true") {
open();

View File

@ -1,3 +1,5 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";
@ -51,13 +53,18 @@ const stepRouteSchema = z.object({
const OnboardingPage = () => {
const pathname = usePathname();
const params = useParamsWithFallback();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
const result = stepRouteSchema.safeParse(params);
const result = stepRouteSchema.safeParse({
...params,
step: Array.isArray(params.step) ? params.step : [params.step],
});
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
const headers = [
{
title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
@ -218,7 +225,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { redirect: { permanent: false, destination: "/event-types" } };
}
const locale = await getLocale(context.req);
return {
props: {
...(await serverSideTranslations(locale, ["common"])),

View File

@ -0,0 +1,33 @@
import { getLayout as getSettingsLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { HorizontalTabs } from "@calcom/ui";
export default function OrgMigrationLayout({ children }: { children: React.ReactElement }) {
return getSettingsLayout(
<div>
<HorizontalTabs
tabs={[
{
name: "Move Team to Org",
href: "/settings/admin/orgMigrations/moveTeamToOrg",
},
{
name: "Move User to Org",
href: "/settings/admin/orgMigrations/moveUserToOrg",
},
{
name: "Revert: Move Team to Org",
href: "/settings/admin/orgMigrations/removeTeamFromOrg",
},
{
name: "Revert: Move User to Org",
href: "/settings/admin/orgMigrations/removeUserFromOrg",
},
]}
/>
{children}
</div>
);
}
export const getLayout = (page: React.ReactElement) => {
return <OrgMigrationLayout>{page}</OrgMigrationLayout>;
};

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