Merge remote-tracking branch 'refs/remotes/origin/feat/needs-help-embed' into feat/needs-help-embed

This commit is contained in:
sean-brydon 2023-12-28 15:54:10 +10:00
commit 0a1dcee728
282 changed files with 5927 additions and 1307 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -9,6 +9,10 @@ env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}

View File

@ -2,7 +2,7 @@
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],

View File

@ -147,7 +147,7 @@ Here is what you need to be able to run Cal.com.
- Duplicate `.env.example` to `.env`
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
5. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
@ -216,7 +216,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
- [Setup postgres DB with render](https://render.com/docs/databases)
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.

View File

@ -41,6 +41,28 @@ test.describe("Org", () => {
await expectPageToBeServerSideRendered(page);
});
});
test.describe("Dynamic Group Booking", () => {
test("Dynamic Group booking link should load", async ({ page }) => {
const users = [
{
username: "peer",
name: "Peer Richelsen",
},
{
username: "bailey",
name: "Bailey Pumfleet",
},
];
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
expect(response?.status()).toBe(200);
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
// 2 users and 1 for the organization(2+1)
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
});
});
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,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";

View File

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

View File

@ -1,5 +1,5 @@
import LegacyPage from "@pages/apps/installed/[category]";
import { _generateMetadata } from "_app/_utils";
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation";
import { z } from "zod";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.6.0",
"version": "3.6.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&apos;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 ">Dont 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;

View File

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

View File

@ -1,3 +1,5 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { GetStaticPaths, GetStaticProps } from "next";
import { Fragment, useState } from "react";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import { getLayout } from "@calcom/features/MainLayout";

View File

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

View File

@ -1,3 +1,5 @@
"use client";
import type { NextPageContext } from "next";
import dayjs from "@calcom/dayjs";

View File

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