Merge remote-tracking branch 'refs/remotes/origin/feat/needs-help-embed' into feat/needs-help-embed
This commit is contained in:
commit
0a1dcee728
20
.env.example
20
.env.example
|
@ -249,6 +249,18 @@ PROJECT_ID_VERCEL=
|
|||
TEAM_ID_VERCEL=
|
||||
# Get it from: https://vercel.com/account/tokens
|
||||
AUTH_BEARER_TOKEN_VERCEL=
|
||||
# Add the main domain that you want to use for testing vercel domain management for organizations. This is necessary because WEBAPP_URL of local isn't a valid public domain
|
||||
# Would create org1.example.com for an org with slug org1
|
||||
# LOCAL_TESTING_DOMAIN_VERCEL="example.com"
|
||||
|
||||
## Set it to 1 if you use cloudflare to manage your DNS and would like us to manage the DNS for you for organizations
|
||||
# CLOUDFLARE_DNS=1
|
||||
## Get it from: https://dash.cloudflare.com/profile/api-tokens. Select Edit Zone template and choose a zone(your domain)
|
||||
# AUTH_BEARER_TOKEN_CLOUDFLARE=
|
||||
## Zone ID can be found in the Overview tab of your domain in Cloudflare
|
||||
# CLOUDFLARE_ZONE_ID=
|
||||
## It should usually work with the default value. This is the DNS CNAME record content to point to Vercel domain
|
||||
# CLOUDFLARE_VERCEL_CNAME=cname.vercel-dns.com
|
||||
|
||||
# - APPLE CALENDAR
|
||||
# Used for E2E tests on Apple Calendar
|
||||
|
@ -260,6 +272,8 @@ E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
|||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS=
|
||||
E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
|
@ -302,3 +316,9 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
|||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_VIDEO_ENABLED=0
|
||||
|
|
|
@ -17,10 +17,15 @@ runs:
|
|||
cache-name: cache-db
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||
- run: yarn db-seed
|
||||
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
|
||||
- name: Postgres Dump Backup
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E App-Store Apps
|
||||
name: E2E App-Store Apps Tests
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
@ -33,6 +33,12 @@ jobs:
|
|||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
|
@ -43,6 +49,10 @@ jobs:
|
|||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E Embed React tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed React tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E Embed Core tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed Core tests and booking flow (for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: E2E test
|
||||
name: E2E tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
@ -24,7 +24,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
|
|
@ -15,35 +15,6 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
|
@ -51,7 +22,7 @@ jobs:
|
|||
|
||||
test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/test.yml
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
|
@ -64,42 +35,13 @@ jobs:
|
|||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [lint, type-check, test, build]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
name: Pre-release checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
|
@ -9,6 +9,10 @@ env:
|
|||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
|
|
|
@ -147,7 +147,7 @@ Here is what you need to be able to run Cal.com.
|
|||
|
||||
- Duplicate `.env.example` to `.env`
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
5. Setup Node
|
||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||
|
@ -216,7 +216,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||
|
||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||
|
||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||
|
|
|
@ -41,6 +41,28 @@ test.describe("Org", () => {
|
|||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
});
|
||||
test.describe("Dynamic Group Booking", () => {
|
||||
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||
const users = [
|
||||
{
|
||||
username: "peer",
|
||||
name: "Peer Richelsen",
|
||||
},
|
||||
{
|
||||
username: "bailey",
|
||||
name: "Bailey Pumfleet",
|
||||
},
|
||||
];
|
||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||
// 2 users and 1 for the organization(2+1)
|
||||
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
|
|
|
@ -11,6 +11,12 @@ const ROUTES: [URLPattern, boolean][] = [
|
|||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
|
||||
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
|
||||
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
|
||||
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
|
||||
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
||||
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { createHydrateClient } from "_app/_trpc/createHydrateClient";
|
||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const HydrateClient = createHydrateClient({
|
|
@ -1,6 +1,6 @@
|
|||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HydrateClient } from "_app/_trpc/HydrateClient";
|
||||
import { trpc } from "_app/_trpc/client";
|
||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
||||
import { trpc } from "app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import AppPage from "@pages/apps/[slug]/index";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { notFound } from "next/navigation";
|
|
@ -1,5 +1,5 @@
|
|||
import SetupPage from "@pages/apps/[slug]/setup";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
|
@ -1,6 +1,6 @@
|
|||
import CategoryPage from "@pages/apps/categories/[category]";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { ssrInit } from "_app/_trpc/ssrInit";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
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";
|
|
@ -1,5 +1,5 @@
|
|||
import LegacyPage from "@pages/apps/installed/[category]";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { ssgInit } from "app/_trpc/ssgInit";
|
||||
import type { Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ReactElement } from "react";
|
||||
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({
|
||||
status: z.enum(validStatuses),
|
||||
});
|
||||
|
||||
type Props = { params: Params; children: ReactElement };
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => `${APP_NAME} | ${t("bookings")}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
const getData = async ({ params }: { params: Params }) => {
|
||||
const parsedParams = querySchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const ssg = await ssgInit();
|
||||
|
||||
return {
|
||||
status: parsedParams.data.status,
|
||||
dehydratedState: await ssg.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
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 const dynamic = "force-static";
|
|
@ -0,0 +1 @@
|
|||
export { default } from "@pages/bookings/[status]";
|
|
@ -0,0 +1,64 @@
|
|||
import OldPage from "@pages/teams/index";
|
||||
import { ssrInit } from "app/_trpc/ssrInit";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
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();
|
||||
|
||||
const session = await getServerSession({
|
||||
req: context.req,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
const token = Array.isArray(context.query.token) ? context.query.token[0] : context.query.token;
|
||||
|
||||
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
|
||||
return redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
|
||||
}
|
||||
|
||||
return { dehydratedState: 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;
|
|
@ -0,0 +1,130 @@
|
|||
import OldPage from "@pages/video/[uid]";
|
||||
import { ssrInit } from "app/_trpc/ssrInit";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
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">) {
|
||||
const ssr = await ssrInit();
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: context.query.uid as string,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
description: true,
|
||||
isRecorded: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organization: {
|
||||
select: {
|
||||
calVideoLogo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
meetingUrl: true,
|
||||
meetingPassword: true,
|
||||
},
|
||||
where: {
|
||||
type: "daily_video",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) {
|
||||
return redirect("/video/no-meeting-found");
|
||||
}
|
||||
|
||||
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
|
||||
const now = new Date();
|
||||
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
//find out if the meeting is in the past
|
||||
const isPast = booking?.endTime <= exitDate;
|
||||
if (isPast) {
|
||||
return redirect(`/video/meeting-ended/${booking?.uid}`);
|
||||
}
|
||||
|
||||
const bookingObj = Object.assign({}, booking, {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
// set meetingPassword to null for guests
|
||||
if (session?.user.id !== bookingObj.user?.id) {
|
||||
bookingObj.references.forEach((bookRef: any) => {
|
||||
bookRef.meetingPassword = null;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
meetingUrl: bookingObj.references[0].meetingUrl ?? "",
|
||||
...(typeof bookingObj.references[0].meetingPassword === "string" && {
|
||||
meetingPassword: bookingObj.references[0].meetingPassword,
|
||||
}),
|
||||
booking: {
|
||||
...bookingObj,
|
||||
...(bookingObj.description && { description: md.render(bookingObj.description) }),
|
||||
},
|
||||
dehydratedState: await ssr.dehydrate(),
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,76 @@
|
|||
import OldPage from "@pages/video/meeting-ended/[uid]";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
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: {
|
||||
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
user: {
|
||||
select: {
|
||||
credentials: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true,
|
||||
meetingUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return redirect("/video/no-meeting-found");
|
||||
}
|
||||
|
||||
const bookingObj = Object.assign({}, booking, {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
booking: bookingObj,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,69 @@
|
|||
import OldPage from "@pages/video/meeting-not-started/[uid]";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
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;
|
||||
}>;
|
||||
|
||||
export const generateMetadata = async ({ params }: PageProps) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: typeof params?.uid === "string" ? params.uid : "",
|
||||
},
|
||||
select: bookingMinimalSelect,
|
||||
});
|
||||
|
||||
return await _generateMetadata(
|
||||
(t) => t("this_meeting_has_not_started_yet"),
|
||||
() => booking?.title ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
|
||||
},
|
||||
select: bookingMinimalSelect,
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return redirect("/video/no-meeting-found");
|
||||
}
|
||||
|
||||
const bookingObj = Object.assign({}, booking, {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
booking: bookingObj,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/apps/[category]";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/apps/index";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/flags";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/impersonation";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/oAuth/index";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/index";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import EventTypes from "@pages/event-types";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -1,5 +1,5 @@
|
|||
import Page from "@pages/settings/admin/oAuth/oAuthView";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/video/no-meeting-found";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "",
|
||||
() => ""
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getServerCaller } from "_app/_trpc/serverClient";
|
||||
import { type Params } from "_app/_types";
|
||||
import { _generateMetadata } from "_app/_utils";
|
||||
import { getServerCaller } from "app/_trpc/serverClient";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-add-view";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { _generateMetadata } from "_app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { DefaultSeo } from "next-seo";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
|
@ -17,13 +18,7 @@ export interface CalPageWrapper {
|
|||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
}
|
||||
|
||||
const interFont = localFont({
|
||||
src: "../fonts/InterVariable.woff2",
|
||||
variable: "--font-inter",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
|
||||
import {
|
||||
getEventLocationType,
|
||||
getSuccessPageLocationMessage,
|
||||
guessEventLocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
|
@ -14,6 +18,7 @@ import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { ActionType } from "@calcom/ui";
|
||||
|
@ -93,6 +98,16 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
|
||||
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||
|
||||
const location = booking.location as ReturnType<typeof getEventLocationValue>;
|
||||
const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl;
|
||||
|
||||
const locationToDisplay = getSuccessPageLocationMessage(
|
||||
locationVideoCallUrl ? locationVideoCallUrl : location,
|
||||
t,
|
||||
booking.status
|
||||
);
|
||||
const provider = guessEventLocationType(location);
|
||||
|
||||
const bookingConfirm = async (confirm: boolean) => {
|
||||
let body = {
|
||||
bookingId: booking.id,
|
||||
|
@ -359,6 +374,33 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
attendees={booking.attendees}
|
||||
/>
|
||||
</div>
|
||||
{!isPending && (
|
||||
<div>
|
||||
{(provider?.label || locationToDisplay?.startsWith("https://")) &&
|
||||
locationToDisplay.startsWith("http") && (
|
||||
<a
|
||||
href={locationToDisplay}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target="_blank"
|
||||
title={locationToDisplay}
|
||||
rel="noreferrer"
|
||||
className="text-sm leading-6 text-blue-600 hover:underline">
|
||||
<div className="flex items-center gap-2">
|
||||
{provider?.iconUrl && (
|
||||
<img
|
||||
src={provider.iconUrl}
|
||||
className="h-4 w-4 rounded-sm"
|
||||
alt={`${provider?.label} logo`}
|
||||
/>
|
||||
)}
|
||||
{provider?.label
|
||||
? t("join_event_location", { eventLocationType: provider?.label })
|
||||
: t("join_meeting")}
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isPending && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("unconfirmed")}
|
||||
|
|
|
@ -47,10 +47,17 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
|
||||
const eventTypeFormMetadata = methods.getValues("metadata");
|
||||
|
||||
const getAppDataSetter = (
|
||||
appId: EventTypeAppsList,
|
||||
appCategories: string[],
|
||||
credentialId?: number
|
||||
): SetAppData => {
|
||||
return function (key, value) {
|
||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||
|
||||
const appData = allAppsDataFromForm[appId];
|
||||
setAllAppsData({
|
||||
...allAppsDataFromForm,
|
||||
|
@ -58,6 +65,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
...appData,
|
||||
[key]: value,
|
||||
credentialId,
|
||||
appCategories,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -77,10 +85,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
appCards.push(
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
||||
setAppData={getAppDataSetter(
|
||||
app.slug as EventTypeAppsList,
|
||||
app.categories,
|
||||
app.userCredentialIds[0]
|
||||
)}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
|
@ -91,7 +104,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
appCards.push(
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.categories, team.credentialId)}
|
||||
key={app.slug + team?.credentialId}
|
||||
app={{
|
||||
...app,
|
||||
|
@ -104,6 +117,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
},
|
||||
}}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
|
@ -148,10 +162,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
return (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
||||
setAppData={getAppDataSetter(
|
||||
app.slug as EventTypeAppsList,
|
||||
app.categories,
|
||||
app.userCredentialIds[0]
|
||||
)}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
|
@ -179,10 +198,11 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
{notInstalledApps?.map((app) => (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.categories)}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
|
||||
import InstantEventController from "./InstantEventController";
|
||||
|
||||
export const EventInstantTab = ({
|
||||
eventType,
|
||||
isTeamEvent,
|
||||
}: Pick<EventTypeSetupProps, "eventType"> & { isTeamEvent: boolean }) => {
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
return (
|
||||
<InstantEventController paymentEnabled={requirePayment} eventType={eventType} isTeamEvent={isTeamEvent} />
|
||||
);
|
||||
};
|
|
@ -44,6 +44,7 @@ export const mapMemberToChildrenOption = (
|
|||
username: member.username ?? "",
|
||||
membership: member.membership,
|
||||
eventTypeSlugs: member.eventTypes ?? [],
|
||||
avatar: member.avatar,
|
||||
},
|
||||
value: `${member.id ?? ""}`,
|
||||
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
|
||||
|
|
|
@ -7,11 +7,9 @@ import { useMemo, useState, Suspense } from "react";
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
@ -67,6 +65,7 @@ type Props = {
|
|||
isUpdateMutationLoading?: boolean;
|
||||
availability?: AvailabilityOption;
|
||||
isUserOrganizationAdmin: boolean;
|
||||
bookerUrl: string;
|
||||
};
|
||||
|
||||
function getNavigation(props: {
|
||||
|
@ -135,6 +134,7 @@ function EventTypeSingleLayout({
|
|||
formMethods,
|
||||
availability,
|
||||
isUserOrganizationAdmin,
|
||||
bookerUrl,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
|
@ -207,13 +207,21 @@ function EventTypeSingleLayout({
|
|||
icon: Users,
|
||||
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
||||
isManagedEventType
|
||||
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
|
||||
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
|
||||
: ""
|
||||
}`,
|
||||
});
|
||||
}
|
||||
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
|
||||
if (showWebhooks) {
|
||||
if (team) {
|
||||
navigation.push({
|
||||
name: "instant_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=instant`,
|
||||
icon: Zap,
|
||||
info: `instant_event_tab_description`,
|
||||
});
|
||||
}
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||
|
@ -235,10 +243,8 @@ function EventTypeSingleLayout({
|
|||
formMethods,
|
||||
]);
|
||||
|
||||
const orgBranding = useOrgBranding();
|
||||
const isOrgEvent = orgBranding?.fullDomain;
|
||||
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
|
||||
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
|
||||
const permalink = `${bookerUrl}/${
|
||||
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
|
||||
}/${eventType.slug}`;
|
||||
|
||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, EmptyScreen, SettingsToggle } from "@calcom/ui";
|
||||
import { Zap } from "@calcom/ui/components/icon";
|
||||
|
||||
type InstantEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
paymentEnabled: boolean;
|
||||
isTeamEvent: boolean;
|
||||
};
|
||||
|
||||
export default function InstantEventController({
|
||||
eventType,
|
||||
paymentEnabled,
|
||||
isTeamEvent,
|
||||
}: InstantEventControllerProps) {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const [instantEventState, setInstantEventState] = useState<boolean>(eventType?.isInstantEvent ?? false);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
const instantLocked = shouldLockDisableProps("isInstantEvent");
|
||||
|
||||
const isOrg = !!session.data?.user?.org?.id;
|
||||
|
||||
if (session.status === "loading") return <></>;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="block items-start sm:flex">
|
||||
{!isOrg || !isTeamEvent ? (
|
||||
<EmptyScreen
|
||||
headline={t("instant_tab_title")}
|
||||
Icon={Zap}
|
||||
description={t("uprade_to_create_instant_bookings")}
|
||||
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className={!paymentEnabled ? "w-full" : ""}>
|
||||
{paymentEnabled ? (
|
||||
<Alert severity="warning" title={t("warning_payment_instant_meeting_event")} />
|
||||
) : (
|
||||
<>
|
||||
<Alert
|
||||
className="mb-4"
|
||||
severity="warning"
|
||||
title={t("warning_instant_meeting_experimental")}
|
||||
/>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
|
||||
instantEventState && "rounded-b-none"
|
||||
)}
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("instant_tab_title")}
|
||||
{...instantLocked}
|
||||
description={t("instant_event_tab_description")}
|
||||
checked={instantEventState}
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("isInstantEvent", false);
|
||||
setInstantEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("isInstantEvent", true);
|
||||
setInstantEventState(true);
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{instantEventState && (
|
||||
<div data-testid="instant-event-collapsible" className="flex flex-col gap-2 text-sm">
|
||||
<p>{t("warning_payment_instant_meeting_event")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
|
@ -82,7 +82,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||
type="button"
|
||||
data-testid="save-calendar-button"
|
||||
className={classNames(
|
||||
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||
"text-inverted bg-inverted border-inverted mt-8 flex w-full flex-row justify-center rounded-md border p-2 text-center text-sm",
|
||||
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
|
||||
)}
|
||||
onClick={() => nextStep()}
|
||||
|
|
|
@ -53,7 +53,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
|||
type="button"
|
||||
data-testid="save-video-button"
|
||||
className={classNames(
|
||||
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||
"text-inverted border-inverted bg-inverted mt-8 flex w-full flex-row justify-center rounded-md border p-2 text-center text-sm",
|
||||
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
|
||||
)}
|
||||
disabled={!hasAnyInstalledVideoApps}
|
||||
|
|
|
@ -152,9 +152,7 @@ const UserProfile = () => {
|
|||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
/>
|
||||
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
||||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
<p className="text-default mt-2 font-sans text-sm font-normal">{t("few_sentences_about_yourself")}</p>
|
||||
</fieldset>
|
||||
<Button EndIcon={ArrowRight} type="submit" className="mt-8 w-full items-center justify-center">
|
||||
{t("finish")}
|
||||
|
|
|
@ -12,7 +12,7 @@ type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
|
|||
type MembersType = TeamType["members"];
|
||||
type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & {
|
||||
safeBio: string | null;
|
||||
orgOrigin: string;
|
||||
bookerUrl: string;
|
||||
};
|
||||
|
||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||
|
@ -26,7 +26,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||
return (
|
||||
<Link
|
||||
key={member.id}
|
||||
href={{ pathname: `${member.orgOrigin}/${member.username}`, query: queryParamsToForward }}>
|
||||
href={{ pathname: `${member.bookerUrl}/${member.username}`, query: queryParamsToForward }}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<UserAvatar size="md" user={member} />
|
||||
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
||||
|
|
|
@ -74,7 +74,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||
const debouncedApiCall = useMemo(
|
||||
() =>
|
||||
debounce(async (username: string) => {
|
||||
const { data } = await fetchUsername(username);
|
||||
// TODO: Support orgSlug
|
||||
const { data } = await fetchUsername(username, null);
|
||||
setMarkAsError(!data.available && !!currentUsername && username !== currentUsername);
|
||||
setIsInputUsernamePremium(data.premium);
|
||||
setUsernameIsAvailable(data.available);
|
||||
|
|
|
@ -44,7 +44,8 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||
const debouncedApiCall = useMemo(
|
||||
() =>
|
||||
debounce(async (username) => {
|
||||
const { data } = await fetchUsername(username);
|
||||
// TODO: Support orgSlug
|
||||
const { data } = await fetchUsername(username, null);
|
||||
setMarkAsError(!data.available);
|
||||
setUsernameIsAvailable(data.available);
|
||||
}, 150),
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { AvatarGroup } from "@calcom/ui";
|
||||
|
||||
type UserAvatarProps = Omit<React.ComponentProps<typeof AvatarGroup>, "items"> & {
|
||||
users: Pick<User, "organizationId" | "name" | "username">[];
|
||||
users: (Pick<User, "organizationId" | "name" | "username"> & { bookerUrl: string })[];
|
||||
organization: Pick<Team, "slug" | "name">;
|
||||
};
|
||||
|
||||
export function UserAvatarGroupWithOrg(props: UserAvatarProps) {
|
||||
const { users, organization, ...rest } = props;
|
||||
const orgBranding = useOrgBranding();
|
||||
const baseUrl = `${orgBranding?.fullDomain ?? CAL_URL}`;
|
||||
const items = [
|
||||
{
|
||||
href: baseUrl,
|
||||
href: getBookerBaseUrlSync(organization.slug),
|
||||
image: `${WEBAPP_URL}/team/${organization.slug}/avatar.png`,
|
||||
alt: organization.name || undefined,
|
||||
title: organization.name,
|
||||
|
@ -23,13 +21,12 @@ export function UserAvatarGroupWithOrg(props: UserAvatarProps) {
|
|||
].concat(
|
||||
users.map((user) => {
|
||||
return {
|
||||
href: `${baseUrl}/${user.username}/?redirect=false`,
|
||||
href: `${user.bookerUrl}/${user.username}?redirect=false`,
|
||||
image: getUserAvatarUrl(user),
|
||||
alt: user.name || undefined,
|
||||
title: user.name || user.username || "",
|
||||
};
|
||||
})
|
||||
);
|
||||
users.unshift();
|
||||
return <AvatarGroup {...rest} items={items} />;
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { TrpcProvider } from "_app/_trpc/trpc-provider";
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { dir } from "i18next";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { type Params } from "app/_types";
|
||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||
|
||||
// returns query object same as ctx.query but for app dir
|
||||
export const getQuery = (url: string, params: Params) => {
|
||||
if (!url.length) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(url);
|
||||
const searchParamsObj = Object.fromEntries(searchParams.entries());
|
||||
|
||||
return { ...searchParamsObj, ...params };
|
||||
};
|
||||
|
||||
export const buildLegacyCtx = (headers: ReadonlyHeaders, cookies: ReadonlyRequestCookies, params: Params) => {
|
||||
return {
|
||||
query: getQuery(headers.get("x-url") ?? "", params),
|
||||
params,
|
||||
req: { headers, cookies },
|
||||
};
|
||||
};
|
|
@ -130,6 +130,18 @@ export const config = {
|
|||
"/future/apps/categories/",
|
||||
"/apps/categories/:category/",
|
||||
"/future/apps/categories/:category/",
|
||||
"/workflows/:path*",
|
||||
"/future/workflows/:path*",
|
||||
"/settings/teams/:path*",
|
||||
"/future/settings/teams/:path*",
|
||||
"/getting-started/:step/",
|
||||
"/future/getting-started/:step/",
|
||||
"/apps",
|
||||
"/future/apps",
|
||||
"/bookings/:status/",
|
||||
"/future/bookings/:status/",
|
||||
"/video/:path*",
|
||||
"/future/video/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -20,6 +20,7 @@ import { getUsernameList } from "@calcom/lib/defaultEvents";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
@ -188,46 +189,55 @@ UserPage.isBookingPage = true;
|
|||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
return (
|
||||
await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: userId,
|
||||
},
|
||||
const eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
{
|
||||
id: "asc",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
select: {
|
||||
...baseEventTypeSelect,
|
||||
metadata: true,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
})
|
||||
).map((eventType) => ({
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
}));
|
||||
{
|
||||
id: "asc",
|
||||
},
|
||||
],
|
||||
select: {
|
||||
...baseEventTypeSelect,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
// map and filter metadata, exclude eventType entirely when faulty metadata is found.
|
||||
// report error to exception so we don't lose the error.
|
||||
return eventTypes.reduce<typeof eventTypes>((eventTypes, eventType) => {
|
||||
const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
|
||||
if (!parsedMetadata.success) {
|
||||
logger.error(parsedMetadata.error);
|
||||
return eventTypes;
|
||||
}
|
||||
eventTypes.push({
|
||||
...eventType,
|
||||
metadata: parsedMetadata.data,
|
||||
});
|
||||
return eventTypes;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export type UserPageProps = {
|
||||
|
|
|
@ -185,7 +185,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req.headers.host ?? "", context.params?.orgSlug),
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleInstantMeeting from "@calcom/features/instant-meeting/handleInstantMeeting";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const userIp = getIP(req);
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: `instant.event-${userIp}`,
|
||||
});
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
req.userId = session?.user?.id || -1;
|
||||
const booking = await handleInstantMeeting(req);
|
||||
return booking;
|
||||
}
|
||||
export default defaultResponder(handler);
|
|
@ -1,4 +1,5 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
|
@ -8,8 +9,14 @@ type Response = {
|
|||
premium: boolean;
|
||||
};
|
||||
|
||||
const bodySchema = z.object({
|
||||
username: z.string(),
|
||||
orgSlug: z.string().optional(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
||||
const { currentOrgDomain } = orgDomainConfig(req);
|
||||
const result = await checkUsername(req.body.username, currentOrgDomain);
|
||||
const { username, orgSlug } = bodySchema.parse(req.body);
|
||||
const result = await checkUsername(username, currentOrgDomain || orgSlug);
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
|
|
@ -173,7 +173,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
|||
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
|
||||
|
||||
return (
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function Authorize() {
|
|||
const router = useRouter();
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
const client_id = searchParams?.get("client_id") as string;
|
||||
const client_id = (searchParams?.get("client_id") as string) || "";
|
||||
const state = searchParams?.get("state") as string;
|
||||
const scope = searchParams?.get("scope") as string;
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { motion } from "framer-motion";
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { AlertTriangle, Check, MailOpen } from "@calcom/ui/components/icon";
|
||||
import { AlertTriangle, ExternalLink, MailOpen } from "@calcom/ui/components/icon";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
@ -54,7 +57,62 @@ const querySchema = z.object({
|
|||
t: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Verify() {
|
||||
const PaymentFailedIcon = () => (
|
||||
<div className="rounded-full bg-orange-900 p-3">
|
||||
<AlertTriangle className="h-6 w-6 flex-shrink-0 p-0.5 font-extralight text-orange-100" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const PaymentSuccess = () => (
|
||||
<div
|
||||
className="rounded-full"
|
||||
style={{
|
||||
padding: "6px",
|
||||
border: "0.6px solid rgba(0, 0, 0, 0.02)",
|
||||
background: "rgba(123, 203, 197, 0.10)",
|
||||
}}>
|
||||
<motion.div
|
||||
className="rounded-full"
|
||||
style={{
|
||||
padding: "6px",
|
||||
border: "0.6px solid rgba(0, 0, 0, 0.04)",
|
||||
background: "rgba(123, 203, 197, 0.16)",
|
||||
}}
|
||||
animate={{ scale: [1, 1.1, 1] }} // Define the pulsing animation for the second ring
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
delay: 0.2, // Delay the start of animation for the second ring
|
||||
}}>
|
||||
<motion.div
|
||||
className="rounded-full p-3"
|
||||
style={{
|
||||
border: "1px solid rgba(255, 255, 255, 0.40)",
|
||||
background: "linear-gradient(180deg, #66C9CF 0%, #9CCCB2 100%)",
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.69185 10.6919L2.9297 10.9297L2.69185 10.6919C1.96938 11.4143 1.96938 12.5857 2.69185 13.3081L7.69185 18.3081C8.41432 19.0306 9.58568 19.0306 10.3081 18.3081L21.3081 7.30815C22.0306 6.58568 22.0306 5.41432 21.3081 4.69185C20.5857 3.96938 19.4143 3.96938 18.6919 4.69185L9 14.3837L5.30815 10.6919C4.58568 9.96938 3.41432 9.96938 2.69185 10.6919Z"
|
||||
fill="white"
|
||||
stroke="#48BAAE"
|
||||
strokeWidth="0.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MailOpenIcon = () => (
|
||||
<div className="bg-default rounded-full p-3">
|
||||
<MailOpen className="text-emphasis h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function Verify(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
@ -112,7 +170,7 @@ export default function Verify() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="text-inverted bg-black bg-opacity-90 backdrop-blur-md backdrop-grayscale backdrop-filter">
|
||||
<div className="text-default bg-muted bg-opacity-90 backdrop-blur-md backdrop-grayscale backdrop-filter">
|
||||
<Head>
|
||||
<title>
|
||||
{/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
|
||||
|
@ -125,17 +183,9 @@ export default function Verify() {
|
|||
</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
|
||||
<div className="rounded-full border border-white p-3">
|
||||
{hasPaymentFailed ? (
|
||||
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
) : sessionId ? (
|
||||
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight dark:text-white" />
|
||||
) : (
|
||||
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-cal text-inverted my-6 text-3xl font-normal dark:text-white">
|
||||
<div className="border-subtle bg-default m-10 flex max-w-2xl flex-col items-center rounded-xl border px-8 py-14 text-left">
|
||||
{hasPaymentFailed ? <PaymentFailedIcon /> : sessionId ? <PaymentSuccess /> : <MailOpenIcon />}
|
||||
<h3 className="font-cal text-emphasis my-6 text-2xl font-normal leading-none">
|
||||
{hasPaymentFailed
|
||||
? "Your payment failed"
|
||||
: sessionId
|
||||
|
@ -145,41 +195,60 @@ export default function Verify() {
|
|||
{hasPaymentFailed && (
|
||||
<p className="my-6">Your account has been created, but your premium has not been reserved.</p>
|
||||
)}
|
||||
<p className="text-inverted dark:text-white">
|
||||
<p className="text-muted dark:text-subtle text-base font-normal">
|
||||
We have sent an email to <b>{customer?.email} </b>with a link to activate your account.{" "}
|
||||
{hasPaymentFailed &&
|
||||
"Once you activate your account you will be able to try purchase your premium username again or select a different one."}
|
||||
</p>
|
||||
<p className="text-muted mt-6">
|
||||
Don't see an email? Click the button below to send another email.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex space-x-5 text-center">
|
||||
<div className="mt-7">
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={secondsLeft > 0}
|
||||
onClick={async (e) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setSecondsLeft(30);
|
||||
// Update query params with t:timestamp, shallow: true doesn't re-render the page
|
||||
const _searchParams = new URLSearchParams(searchParams?.toString());
|
||||
_searchParams.set("t", `${Date.now()}`);
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
return await sendVerificationLogin(customer.email, customer.username);
|
||||
}}>
|
||||
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}
|
||||
</Button>
|
||||
<Button color="primary" href={`${WEBAPP_URL || "https://app.cal.com"}/auth/login`}>
|
||||
Login using another method
|
||||
href={
|
||||
props.EMAIL_FROM
|
||||
? encodeURIComponent(`https://mail.google.com/mail/u/0/#search/from:${props.EMAIL_FROM}`)
|
||||
: "https://mail.google.com/mail/u/0/"
|
||||
}
|
||||
target="_blank"
|
||||
EndIcon={ExternalLink}>
|
||||
Open in Gmail
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-subtle text-base font-normal ">Don’t seen an email?</p>
|
||||
<button
|
||||
className={classNames(
|
||||
"font-light",
|
||||
secondsLeft > 0 ? "text-muted" : "underline underline-offset-2 hover:font-normal"
|
||||
)}
|
||||
disabled={secondsLeft > 0}
|
||||
onClick={async (e) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setSecondsLeft(30);
|
||||
// Update query params with t:timestamp, shallow: true doesn't re-render the page
|
||||
const _searchParams = new URLSearchParams(searchParams?.toString());
|
||||
_searchParams.set("t", `${Date.now()}`);
|
||||
router.replace(`${pathname}?${_searchParams.toString()}`);
|
||||
return await sendVerificationLogin(customer.email, customer.username);
|
||||
}}>
|
||||
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Resend"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const EMAIL_FROM = process.env.EMAIL_FROM;
|
||||
|
||||
return {
|
||||
props: {
|
||||
EMAIL_FROM,
|
||||
},
|
||||
};
|
||||
};
|
||||
Verify.PageWrapper = PageWrapper;
|
||||
|
|
|
@ -575,7 +575,7 @@ export default function Success(props: SuccessProps) {
|
|||
className="text-default break-words"
|
||||
data-testid="field-response"
|
||||
data-fob-field={field.name}>
|
||||
{response.toString()}
|
||||
{field.type === "boolean" ? (response ? t("yes") : t("no")) : response.toString()}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import { Fragment, useState } from "react";
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { TRPCClientError } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
import { Zap } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
function ConnectAndJoin() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const token = getQueryParam("token");
|
||||
const [meetingUrl, setMeetingUrl] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
|
||||
const session = useSession();
|
||||
const isUserPartOfOrg = session.status === "authenticated" && !!session.data.user?.org;
|
||||
|
||||
const mutation = trpc.viewer.connectAndJoin.useMutation({
|
||||
onSuccess: (res) => {
|
||||
if (res.meetingUrl && !res.isBookingAlreadyAcceptedBySomeoneElse) {
|
||||
router.push(res.meetingUrl);
|
||||
} else if (res.isBookingAlreadyAcceptedBySomeoneElse && res.meetingUrl) {
|
||||
setMeetingUrl(res.meetingUrl);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
console.log("err", err, err instanceof TRPCClientError);
|
||||
if (err instanceof TRPCClientError) {
|
||||
setErrorMessage(t(err.message));
|
||||
} else {
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (session.status === "loading") return <p>{t("loading")}</p>;
|
||||
|
||||
if (!token) return <p>{t("token_not_found")}</p>;
|
||||
|
||||
return (
|
||||
<div className="mx-8 mt-12 block items-start sm:flex">
|
||||
{session ? (
|
||||
<EmptyScreen
|
||||
headline={t("instant_tab_title")}
|
||||
Icon={Zap}
|
||||
description={t("uprade_to_create_instant_bookings")}
|
||||
buttonRaw={
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
{meetingUrl ? (
|
||||
<div className="text-default flex flex-col items-center gap-2 text-center text-sm font-normal">
|
||||
<Trans i18nKey="some_other_host_already_accepted_the_meeting">
|
||||
Some other host already accepted the meeting. Do you still want to join?
|
||||
<Link className="inline-block cursor-pointer underline" href={meetingUrl}>
|
||||
Continue to Meeting Url
|
||||
</Link>
|
||||
</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
tooltip={isUserPartOfOrg ? t("join_meeting") : t("not_part_of_org")}
|
||||
disabled={!isUserPartOfOrg}
|
||||
onClick={() => {
|
||||
mutation.mutate({ token });
|
||||
}}>
|
||||
{t("join_meeting")}
|
||||
</Button>
|
||||
)}
|
||||
{errorMessage && <Alert severity="error" message={errorMessage} />}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div>{t("you_must_be_logged_in_to", { url: WEBAPP_URL })}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ConnectAndJoin.requiresLicense = true;
|
||||
ConnectAndJoin.PageWrapper = PageWrapper;
|
||||
|
||||
export default ConnectAndJoin;
|
|
@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
|
@ -63,6 +64,10 @@ const EventAdvancedTab = dynamic(() =>
|
|||
import("@components/eventtype/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
|
||||
);
|
||||
|
||||
const EventInstantTab = dynamic(() =>
|
||||
import("@components/eventtype/EventInstantTab").then((mod) => mod.EventInstantTab)
|
||||
);
|
||||
|
||||
const EventRecurringTab = dynamic(() =>
|
||||
import("@components/eventtype/EventRecurringTab").then((mod) => mod.EventRecurringTab)
|
||||
);
|
||||
|
@ -84,6 +89,7 @@ export type FormValues = {
|
|||
eventTitle: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
|
@ -149,6 +155,7 @@ const querySchema = z.object({
|
|||
"availability",
|
||||
"apps",
|
||||
"limits",
|
||||
"instant",
|
||||
"recurring",
|
||||
"team",
|
||||
"advanced",
|
||||
|
@ -248,6 +255,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
title: eventType.title,
|
||||
locations: eventType.locations || [],
|
||||
recurringEvent: eventType.recurringEvent || null,
|
||||
isInstantEvent: eventType.isInstantEvent,
|
||||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule || undefined,
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
|
@ -410,6 +418,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
apps: <EventAppsTab eventType={{ ...eventType, URL: permalink }} />,
|
||||
workflows: (
|
||||
|
@ -476,6 +485,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Prevent two payment apps to be enabled
|
||||
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
|
||||
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
|
@ -522,6 +536,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||
disableBorder={true}
|
||||
currentUserMembership={currentUserMembership}
|
||||
bookerUrl={eventType.bookerUrl}
|
||||
isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
|
@ -575,6 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent two payment apps to be enabled
|
||||
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { User } from "@prisma/client";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
@ -19,8 +18,7 @@ import { DuplicateDialog } from "@calcom/features/eventtypes/components/Duplicat
|
|||
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, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { APP_NAME, WEBAPP_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";
|
||||
|
@ -33,7 +31,6 @@ import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
|||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
|
@ -85,7 +82,7 @@ interface EventTypeListHeadingProps {
|
|||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
teamId?: number | null;
|
||||
orgSlug?: string;
|
||||
bookerUrl: string;
|
||||
}
|
||||
|
||||
type EventTypeGroup = EventTypeGroups[number];
|
||||
|
@ -95,6 +92,7 @@ interface EventTypeListProps {
|
|||
group: EventTypeGroup;
|
||||
groupIndex: number;
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
types: EventType[];
|
||||
}
|
||||
|
||||
|
@ -127,6 +125,7 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
|||
types={events[0].eventTypes}
|
||||
group={events[0]}
|
||||
groupIndex={0}
|
||||
bookerUrl={events[0].bookerUrl}
|
||||
readOnly={events[0].metadata.readOnly}
|
||||
/>
|
||||
) : (
|
||||
|
@ -207,12 +206,17 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||
|
||||
const MemoizedItem = memo(Item);
|
||||
|
||||
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
|
||||
export const EventTypeList = ({
|
||||
group,
|
||||
groupIndex,
|
||||
readOnly,
|
||||
types,
|
||||
bookerUrl,
|
||||
}: EventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const orgBranding = useOrgBranding();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
|
@ -383,7 +387,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
|
||||
{types.map((type, index) => {
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
|
@ -410,17 +414,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
/>
|
||||
)}
|
||||
{isManagedEventType && type?.children && type.children?.length > 0 && (
|
||||
<AvatarGroup
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={type?.children
|
||||
.flatMap((ch) => ch.users)
|
||||
.map((user: Pick<User, "name" | "username">) => ({
|
||||
alt: user.name || "",
|
||||
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
title: user.name || "",
|
||||
}))}
|
||||
users={type?.children.flatMap((ch) => ch.users) ?? []}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||
|
@ -697,10 +695,10 @@ const EventTypeListHeading = ({
|
|||
profile,
|
||||
membershipCount,
|
||||
teamId,
|
||||
bookerUrl,
|
||||
}: EventTypeListHeadingProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
|
||||
onSuccess(data) {
|
||||
|
@ -710,18 +708,13 @@ const EventTypeListHeading = ({
|
|||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
const bookerUrl = useBookerUrl();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Avatar
|
||||
alt={profile?.name || ""}
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
imageSrc={
|
||||
orgBranding?.fullDomain
|
||||
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
|
||||
: profile.image
|
||||
}
|
||||
imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`}
|
||||
size="md"
|
||||
className="mt-1 inline-flex justify-center"
|
||||
/>
|
||||
|
@ -742,9 +735,7 @@ const EventTypeListHeading = ({
|
|||
</span>
|
||||
)}
|
||||
{profile?.slug && (
|
||||
<Link
|
||||
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
|
||||
className="text-subtle block text-xs">
|
||||
<Link href={`${bookerUrl}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
|
@ -865,18 +856,22 @@ const Main = ({
|
|||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
<div className="mt-4 flex flex-col" key={group.profile.slug}>
|
||||
<div
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
orgSlug={orgBranding?.slug}
|
||||
bookerUrl={group.bookerUrl}
|
||||
/>
|
||||
|
||||
{group.eventTypes.length ? (
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
bookerUrl={group.bookerUrl}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
|
@ -895,6 +890,7 @@ const Main = ({
|
|||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
bookerUrl={data.eventTypeGroups[0].bookerUrl}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import { z } from "zod";
|
|||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
|
@ -105,7 +106,12 @@ const OnboardingPage = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]"
|
||||
className={classNames(
|
||||
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]"
|
||||
)}
|
||||
data-testid="onboarding"
|
||||
key={pathname}>
|
||||
<Head>
|
||||
|
|
|
@ -14,15 +14,15 @@ import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../te
|
|||
|
||||
const paramsSchema = z.object({
|
||||
orgSlug: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => slugify(s)),
|
||||
user: z.string(),
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const { user: teamOrUserSlug, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const { user: teamOrUserSlugOrDynamicGroup, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: teamOrUserSlug,
|
||||
slug: slugify(teamOrUserSlugOrDynamicGroup),
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
});
|
||||
|
||||
if (team) {
|
||||
const params = { slug: teamOrUserSlug, type };
|
||||
const params = { slug: teamOrUserSlugOrDynamicGroup, type };
|
||||
return GSSTeamTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
@ -47,7 +47,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
},
|
||||
});
|
||||
}
|
||||
const params = { user: teamOrUserSlug, type };
|
||||
const params = { user: teamOrUserSlugOrDynamicGroup, type };
|
||||
return GSSUserTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isEmbed,
|
||||
isBrandingHidden,
|
||||
entity,
|
||||
duration,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
isInstantMeeting
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
Type.isBookingPage = true;
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||
const { duration: queryDuration } = context.query;
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
...getSlugOrRequestedSlug(teamSlug),
|
||||
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: teamSlug,
|
||||
eventSlug: meetingSlug,
|
||||
isTeamEvent: true,
|
||||
org,
|
||||
});
|
||||
|
||||
if (!eventData || !org) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
booking: null,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
teamId: team.id,
|
||||
slug: meetingSlug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
themeBasis: null,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
|
||||
export default function Type() {
|
||||
// Just redirect to the schedule page to reschedule it.
|
||||
|
@ -63,6 +64,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
dynamicEventSlugRef: true,
|
||||
dynamicGroupSlugRef: true,
|
||||
user: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";
|
||||
|
@ -73,6 +75,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
} as const;
|
||||
}
|
||||
|
||||
// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
|
||||
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/booking/${uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import OrgEditView from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = OrgEditView as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
|
@ -73,13 +73,16 @@ function UsernameField({
|
|||
setPremium,
|
||||
premium,
|
||||
setUsernameTaken,
|
||||
orgSlug,
|
||||
usernameTaken,
|
||||
disabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TextField> & {
|
||||
username: string;
|
||||
setPremium: (value: boolean) => void;
|
||||
premium: boolean;
|
||||
usernameTaken: boolean;
|
||||
orgSlug?: string;
|
||||
setUsernameTaken: (value: boolean) => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
@ -90,22 +93,33 @@ function UsernameField({
|
|||
if (formState.isSubmitting || formState.isSubmitSuccessful) return;
|
||||
|
||||
async function checkUsername() {
|
||||
// If the username can't be changed, there is no point in doing the username availability check
|
||||
if (disabled) return;
|
||||
if (!debouncedUsername) {
|
||||
setPremium(false);
|
||||
setUsernameTaken(false);
|
||||
return;
|
||||
}
|
||||
fetchUsername(debouncedUsername).then(({ data }) => {
|
||||
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
|
||||
setPremium(data.premium);
|
||||
setUsernameTaken(!data.available);
|
||||
});
|
||||
}
|
||||
checkUsername();
|
||||
}, [debouncedUsername, setPremium, setUsernameTaken, formState.isSubmitting, formState.isSubmitSuccessful]);
|
||||
}, [
|
||||
debouncedUsername,
|
||||
setPremium,
|
||||
disabled,
|
||||
orgSlug,
|
||||
setUsernameTaken,
|
||||
formState.isSubmitting,
|
||||
formState.isSubmitSuccessful,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
{...register("username")}
|
||||
data-testid="signup-usernamefield"
|
||||
|
@ -229,7 +243,13 @@ export default function Signup({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand-emphasis:#101010] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] [--cal-brand-subtle:#9CA3AF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]">
|
||||
<div
|
||||
className={classNames(
|
||||
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||
"[--cal-brand-subtle:#9CA3AF]",
|
||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
|
||||
)}>
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
{/* Left side */}
|
||||
|
@ -270,6 +290,7 @@ export default function Signup({
|
|||
{/* Username */}
|
||||
{!isOrgInviteByLink ? (
|
||||
<UsernameField
|
||||
orgSlug={orgSlug}
|
||||
label={t("username")}
|
||||
username={watch("username") || ""}
|
||||
premium={premiumUsername}
|
||||
|
@ -604,15 +625,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
|
||||
};
|
||||
|
||||
const isATeamInOrganization = tokenTeam?.parentId !== null;
|
||||
const isOrganization = tokenTeam.metadata?.isOrganization;
|
||||
// Detect if the team is an org by either the metadata flag or if it has a parent team
|
||||
const isOrganization = tokenTeam.metadata?.isOrganization || tokenTeam?.parentId !== null;
|
||||
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
|
||||
// If we are dealing with an org, the slug may come from the team itself or its parent
|
||||
const orgSlug = isOrganization
|
||||
const orgSlug = isOrganizationOrATeamInOrganization
|
||||
? tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug || tokenTeam.slug
|
||||
: null;
|
||||
|
||||
// Org context shouldn't check if a username is premium
|
||||
if (!IS_SELF_HOSTED && !isOrganization) {
|
||||
if (!IS_SELF_HOSTED && !isOrganizationOrATeamInOrganization) {
|
||||
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
|
||||
const { available, suggestion } = await checkPremiumUsername(username);
|
||||
|
||||
|
@ -620,7 +643,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
}
|
||||
|
||||
const isValidEmail = checkValidEmail(verificationToken.identifier);
|
||||
const isOrgInviteByLink = isOrganization && !isValidEmail;
|
||||
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
|
||||
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
|
||||
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
|
||||
: null;
|
||||
|
@ -632,7 +655,14 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
prepopulateFormValues: !isOrgInviteByLink
|
||||
? {
|
||||
email: verificationToken.identifier,
|
||||
username: slugify(username),
|
||||
username: isOrganizationOrATeamInOrganization
|
||||
? getOrgUsernameFromEmail(
|
||||
verificationToken.identifier,
|
||||
(isOrganization
|
||||
? tokenTeam.metadata?.orgAutoAcceptEmail
|
||||
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
|
||||
)
|
||||
: slugify(username),
|
||||
}
|
||||
: null,
|
||||
orgSlug,
|
||||
|
|
|
@ -11,10 +11,11 @@ import { usePathname } from "next/navigation";
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { orgDomainConfig, getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
|
@ -364,7 +365,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
accepted: member.accepted,
|
||||
organizationId: member.organizationId,
|
||||
safeBio: markdownToSafeHTML(member.bio || ""),
|
||||
orgOrigin: getOrgFullOrigin(member.organization?.slug || ""),
|
||||
bookerUrl: getBookerBaseUrlSync(member.organization?.slug || ""),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
|
|
@ -31,6 +31,7 @@ export default function Type({
|
|||
isBrandingHidden,
|
||||
entity,
|
||||
duration,
|
||||
isInstantMeeting,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
|
@ -48,6 +49,7 @@ export default function Type({
|
|||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
isAway={away}
|
||||
isInstantMeeting={isInstantMeeting}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={entity}
|
||||
|
@ -71,7 +73,7 @@ const paramsSchema = z.object({
|
|||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getServerSession(context);
|
||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||
const { rescheduleUid, duration: queryDuration, isInstantMeeting: queryIsInstantMeeting } = context.query;
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
@ -143,6 +145,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
slug: meetingSlug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
@ -19,7 +21,7 @@ import PageWrapper from "@components/PageWrapper";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export type JoinCallPageProps = Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">;
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
export default function JoinCall(props: JoinCallPageProps) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { NextPageContext } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { NextPageContext } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user