chore: App router migration - Create/Use EventRepository and Extract /team/**/* and /teams/* into /modules from /pages (#16453)

This commit is contained in:
Benny Joo
2024-09-12 18:58:08 -03:00
committed by GitHub
parent 1722edbd36
commit d9133cb395
33 changed files with 628 additions and 626 deletions
-4
View File
@@ -14,10 +14,6 @@ export default function withEmbedSsrAppDir<T extends Record<string, any>>(
) {
return async (context: GetServerSidePropsContext): Promise<T> => {
const { embed, layout } = context.query;
const isCOEPEnabled = context.query["flag.coep"] === "true";
if (isCOEPEnabled) {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
try {
const props = await getData(context);
+15 -13
View File
@@ -4,6 +4,9 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import LegacyPage from "~/users/views/users-type-public-view";
@@ -13,21 +16,20 @@ import {
} from "~/users/views/users-type-public-view.getServerSideProps";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { eventData, booking, user, slug } = props;
const { booking, user: username, slug: eventSlug } = props;
const rescheduleUid = booking?.uid;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{
username: user,
eventSlug: slug,
isTeamEvent: false,
org: eventData.entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: eventData.entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false }
);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: false,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
+17 -13
View File
@@ -5,6 +5,9 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
@@ -15,21 +18,22 @@ export const generateMetadata = async ({
params: Params;
searchParams: SearchParams;
}) => {
const pageProps = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const pageProps = await getData(legacyCtx);
const { entity, booking, user, slug, isTeamEvent } = pageProps;
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
const rescheduleUid = booking?.uid;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{
username: user ?? "",
eventSlug: slug ?? "",
isTeamEvent,
org: entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false }
);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent,
org,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
@@ -1,5 +1,3 @@
import { type PageProps } from "@pages/team/[slug]";
import EmbedPage from "@pages/team/[slug]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { Params, SearchParams } from "app/_types";
@@ -10,6 +8,9 @@ import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export const generateMetadata = async ({
@@ -31,7 +32,7 @@ export const generateMetadata = async ({
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({
Page: EmbedPage,
Page: TeamPage,
getData: getEmbedData,
getLayout: null,
isBookingPage: true,
+3 -1
View File
@@ -1,4 +1,3 @@
import TeamPage, { type PageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
@@ -8,6 +7,9 @@ import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({
params,
searchParams,
@@ -1,13 +1,18 @@
import TypePage, { type PageProps } from "@pages/team/[slug]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { PageProps } from "~/team/type-view";
import TypePage from "~/team/type-view";
export const generateMetadata = async ({
params,
searchParams,
@@ -17,19 +22,16 @@ export const generateMetadata = async ({
}) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { eventData, user, slug, booking } = props;
const entity = eventData.entity;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{
username: user,
eventSlug: slug,
isTeamEvent: false,
org: entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false }
);
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: true,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
@@ -1,4 +1,3 @@
import TeamPage, { type PageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
@@ -8,6 +7,9 @@ import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({
params,
searchParams,
@@ -1,10 +1,43 @@
import LegacyPage, { getServerSideProps } from "@pages/team/[slug]/[type]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
const getData = withAppDirSsr(getServerSideProps);
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { PageProps } from "~/team/type-view";
import LegacyPage from "~/team/type-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: true,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
// @ts-expect-error Type is missing the following properties from type: entity, duration, booking, away, and 7 more.
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+16 -14
View File
@@ -1,28 +1,30 @@
import LegacyPage, { type PageProps as LegacyPageProps, getServerSideProps } from "@pages/team/[slug]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { eventData, user, slug, booking } = props;
const entity = eventData.entity;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{
username: user,
eventSlug: slug,
isTeamEvent: false,
org: entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false }
);
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: true,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
+19 -1
View File
@@ -1,7 +1,25 @@
import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const teamName = props.team.name || "Nameless Team";
return await _generateMetadata(
() => teamName,
() => teamName
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
+7 -5
View File
@@ -1,13 +1,16 @@
import LegacyPage, { getServerSideProps, type PageProps as LegacyPageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const teamName = props.team.name || "Nameless Team";
@@ -17,11 +20,10 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
);
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
+8 -2
View File
@@ -1,14 +1,20 @@
import Page from "@pages/teams/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/teams/getServerSideProps";
import type { PageProps } from "~/teams/teams-view";
import Page from "~/teams/teams-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout: null, Page })<"P">;
export default WithLayout({
getData: withAppDirSsr<PageProps>(getServerSideProps),
getLayout: null,
Page,
})<"P">;
@@ -1,10 +1,11 @@
import { getServerSideProps as GSSUserPage } from "@pages/[user]";
import { getServerSideProps as GSSTeamPage } from "@pages/team/[slug]";
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import prisma from "@calcom/prisma";
import { getServerSideProps as GSSTeamPage } from "@lib/team/[slug]/getServerSideProps";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
where: {
+1 -1
View File
@@ -7,7 +7,7 @@ import { ssrInit } from "@server/lib/ssr";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({ req: context.req, res: context.res });
const session = await getServerSession({ req: context.req });
const token = Array.isArray(context.query?.token) ? context.query.token[0] : context.query?.token;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
-132
View File
@@ -133,24 +133,6 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
redirectUrl: "/reschedule",
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
describe("when redirect destination is absolute, should add /embed to end of the path", () => {
@@ -224,23 +206,6 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
redirectUrl: "https://calcom.cal.local/owner",
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
});
@@ -268,85 +233,6 @@ describe("withEmbedSsr", () => {
},
});
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is false', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "false",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is not set', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
it('should not add Cross-Origin-Embedder-Policy header when "flag.coep" query param is set to something other than true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
props: {
prop1: "value1",
},
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "abasdf",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBeUndefined();
});
});
describe("when gSSP doesn't have props or redirect ", () => {
@@ -368,23 +254,5 @@ describe("withEmbedSsr", () => {
expect(ret).toEqual({ notFound: true });
});
it('should add Cross-Origin-Embedder-Policy header when "flag.coep" query param is true', async () => {
const withEmbedGetSsr = withEmbedSsr(
getServerSidePropsFnGenerator({
notFound: true,
})
);
const context = getServerSidePropsContextArg({
embedRelatedParams: {
layout: "week_view",
embed: "",
"flag.coep": "true",
},
});
await withEmbedGetSsr(context);
expect(context.res.getHeader("Cross-Origin-Embedder-Policy")).toBe("require-corp");
});
});
});
-4
View File
@@ -11,10 +11,6 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
const ssrResponse = await getServerSideProps(context);
const embed = context.query.embed;
const layout = context.query.layout;
const isCOEPEnabled = context.query["flag.coep"] === "true";
if (isCOEPEnabled) {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
if ("redirect" in ssrResponse) {
const destinationUrl = ssrResponse.redirect.destination;
+44 -17
View File
@@ -41,21 +41,9 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
}
}
const res = routingForms.handle(url);
const { nonce } = csp(req, res ?? null);
if (!process.env.CSP_POLICY) {
req.headers.set("x-csp", "not-opted-in");
} else if (!req.headers.get("x-csp")) {
// If x-csp not set by gSSP, then it's initialPropsOnly
req.headers.set("x-csp", "initialPropsOnly");
} else {
req.headers.set("x-csp", nonce ?? "");
}
if (res) {
return res;
const routingFormRewriteResponse = routingForms.handleRewrite(url);
if (routingFormRewriteResponse) {
return responseWithHeaders({ url, res: routingFormRewriteResponse, req });
}
if (url.pathname.startsWith("/api/trpc/")) {
@@ -66,6 +54,7 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const isSignupDisabled = await safeGet<boolean>("isSignupDisabled");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isSignupDisabled) {
// TODO: Consider using responseWithHeaders here
return NextResponse.json({ error: "Signup is disabled" }, { status: 503 });
}
}
@@ -88,6 +77,7 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const nextUrl = url.clone();
nextUrl.pathname = validPathname;
// TODO: Consider using responseWithHeaders here
return NextResponse.redirect(nextUrl, { headers: requestHeaders });
}
}
@@ -105,15 +95,17 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
requestHeaders.set("x-locale", locale);
return NextResponse.next({
const res = NextResponse.next({
request: {
headers: requestHeaders,
},
});
return responseWithHeaders({ url, res, req });
};
const routingForms = {
handle: (url: URL) => {
handleRewrite: (url: URL) => {
// Don't 404 old routing_forms links
if (url.pathname.startsWith("/apps/routing_forms")) {
url.pathname = url.pathname.replace(/^\/apps\/routing_forms($|\/)/, "/apps/routing-forms/");
@@ -122,6 +114,41 @@ const routingForms = {
},
};
const embeds = {
addResponseHeaders: ({ url, res }: { url: URL; res: NextResponse }) => {
if (!url.pathname.endsWith("/embed")) {
return res;
}
const isCOEPEnabled = url.searchParams.get("flag.coep") === "true";
if (isCOEPEnabled) {
res.headers.set("Cross-Origin-Embedder-Policy", "require-corp");
}
return res;
},
};
const contentSecurityPolicy = {
addResponseHeaders: ({ res, req }: { res: NextResponse; req: NextRequest }) => {
const { nonce } = csp(req, res ?? null);
if (!process.env.CSP_POLICY) {
res.headers.set("x-csp", "not-opted-in");
} else if (!res.headers.get("x-csp")) {
// If x-csp not set by gSSP, then it's initialPropsOnly
res.headers.set("x-csp", "initialPropsOnly");
} else {
res.headers.set("x-csp", nonce ?? "");
}
return res;
},
};
function responseWithHeaders({ url, res, req }: { url: URL; res: NextResponse; req: NextRequest }) {
const resWithCSP = contentSecurityPolicy.addResponseHeaders({ res, req });
const resWithEmbeds = embeds.addResponseHeaders({ url, res: resWithCSP });
return resWithEmbeds;
}
export const config = {
// Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here.
// https://github.com/vercel/next.js/discussions/42458
+265
View File
@@ -0,0 +1,265 @@
"use client";
// This route is reachable by
// 1. /team/[slug]
// 2. / (when on org domain e.g. http://calcom.cal.com/. This is through a rewrite from next.config.js)
// Also the getServerSideProps and default export are reused by
// 1. org/[orgSlug]/team/[slug]
// 2. org/[orgSlug]/[user]/[type]
import classNames from "classnames";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import Team from "@components/team/screens/Team";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({
team,
considerUnpublished,
markdownStrippedBio,
isValidOrgDomain,
currentOrgDomain,
}: PageProps) {
useTheme(team.theme);
const routerQuery = useRouterQuery();
const pathname = usePathname();
const showMembers = useToggleQuery("members");
const { t } = useLocale();
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
const teamName = team.name || "Nameless Team";
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
const metadata = teamMetadataSchema.parse(team.metadata);
const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);
useEffect(() => {
telemetry.event(
telemetryEventTypes.pageView,
collectPageParameters("/team/[slug]", { isTeamBooking: true })
);
}, [telemetry, pathname]);
if (considerUnpublished) {
const teamSlug = team.slug || metadata?.requestedSlug;
const parentSlug = team.parent?.slug || team.parent?.requestedSlug;
// Show unpublished state for parent Organization itself, if the team is a subteam(team.parent is NOT NULL)
const slugPropertyName = team.parent || team.isOrganization ? "orgSlug" : "teamSlug";
return (
<div className="flex h-full min-h-[calc(100dvh)] items-center justify-center">
<UnpublishedEntity
{...{ [slugPropertyName]: team.parent ? parentSlug : teamSlug }}
logoUrl={team.parent?.logoUrl || team.logoUrl}
name={team.parent ? team.parent.name : team.name}
/>
</div>
);
}
// slug is a route parameter, we don't want to forward it to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
const EventTypes = ({ eventTypes }: { eventTypes: NonNullable<(typeof team)["eventTypes"]> }) => (
<ul className="border-subtle rounded-md border">
{eventTypes.map((type, index) => (
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 bg-default hover:bg-muted border-subtle group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0",
!isEmbed && "bg-default"
)}>
<div className="px-6 py-4 ">
<Link
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}/${type.slug}`,
query: queryParamsToForward,
}}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link"
className="flex justify-between">
<div className="flex-shrink">
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
<h2 className=" text-default text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
<UserAvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
users={type.users}
/>
</div>
</Link>
</div>
</li>
))}
</ul>
);
const SubTeams = () =>
team.children.length ? (
<ul className="divide-subtle border-subtle bg-default !static w-full divide-y rounded-md border">
{team.children.map((ch, i) => {
const memberCount = team.members.filter(
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
).length;
return (
<li key={i} className="hover:bg-muted w-full rounded-md transition">
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
<div className="flex items-center px-5 py-5">
<div className="ms-3 inline-block truncate">
<span className="text-default text-sm font-bold">{ch.name}</span>
<span className="text-subtle block text-xs">
{t("number_member", {
count: memberCount,
})}
</span>
</div>
</div>
<UserAvatarGroup
className="mr-6"
size="sm"
truncateAfter={4}
users={team.members.filter((mem) => mem.subteams?.includes(ch.slug) && mem.accepted)}
/>
</Link>
</li>
);
})}
</ul>
) : (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-emphasis mb-2 text-3xl">{` ${t("org_no_teams_yet")}`}</h2>
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
</div>
</div>
</div>
);
const profileImageSrc = getOrgOrTeamAvatar(team);
return (
<>
<HeadSeo
origin={getOrgFullOrigin(currentOrgDomain)}
title={teamName}
description={teamName}
meeting={{
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: profileImageSrc,
},
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
)}
</div>
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
</>
)}
</main>
</>
);
}
TeamPage.isBookingPage = true;
export default TeamPage;
+75
View File
@@ -0,0 +1,75 @@
"use client";
import { useSearchParams } from "next/navigation";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import type { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export const getMultipleDurationValue = (
multipleDurationConfig: number[] | undefined,
queryDuration: string | string[] | null | undefined,
defaultValue: number
) => {
if (!multipleDurationConfig) return null;
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
return defaultValue;
};
function Type({
slug,
user,
booking,
isEmbed,
isBrandingHidden,
eventData,
isInstantMeeting,
orgBannerUrl,
teamMemberEmail,
}: PageProps) {
const searchParams = useSearchParams();
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
entity={eventData.entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
bookingData={booking}
isInstantMeeting={isInstantMeeting}
hideBranding={isBrandingHidden}
isTeamEvent
entity={eventData.entity}
durationConfig={eventData.metadata?.multipleDuration}
/* TODO: Currently unused, evaluate it is needed-
* Possible alternative approach is to have onDurationChange.
*/
duration={getMultipleDurationValue(
eventData.metadata?.multipleDuration,
searchParams?.get("duration"),
eventData.length
)}
orgBannerUrl={orgBannerUrl}
teamMemberEmail={teamMemberEmail}
/>
</main>
);
}
Type.isBookingPage = true;
export default Type;
+45
View File
@@ -0,0 +1,45 @@
"use client";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button } from "@calcom/ui";
import type { getServerSideProps } from "@lib/teams/getServerSideProps";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
function Teams(props: PageProps) {
const { t } = useLocale();
const [user] = trpc.viewer.me.useSuspenseQuery();
return (
<Shell
withoutMain={false}
heading={t("teams")}
title="Teams"
description="Create and manage teams to use collaborative features."
hideHeadingOnMobile
subtitle={t("create_manage_teams_collaborative")}
CTA={
(!user.organizationId || user.organization.isOrgAdmin) && (
<Button
data-testid="new-team-btn"
variant="fab"
StartIcon="plus"
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
</Button>
)
}>
<TeamsListing />
</Shell>
);
}
Teams.requiresLicense = false;
export default Teams;
@@ -2,12 +2,11 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view.getServerSideProps";
import type { PageProps as TeamTypePageProps } from "../../../../team/[slug]/[type]";
import TeamTypePage from "../../../../team/[slug]/[type]";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
export type PageProps = UserTypePageProps | TeamTypePageProps;
@@ -2,12 +2,11 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import { type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
import type { PageProps as TeamPageProps } from "../../../team/[slug]";
import TeamPage from "../../../team/[slug]";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
export type PageProps = UserPageProps | TeamPageProps;
+1 -1
View File
@@ -1 +1 @@
export { getServerSideProps, default } from "../../team/[slug]";
export { getServerSideProps, default } from "@pages/team/[slug]";
@@ -1 +1 @@
export { getServerSideProps, default } from "../../../../team/[slug]/[type]";
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
@@ -1 +1 @@
export { getServerSideProps, default } from "../../../../team/[slug]";
export { getServerSideProps, default } from "@pages/team/[slug]";
+5 -264
View File
@@ -1,269 +1,10 @@
"use client";
// This route is reachable by
// 1. /team/[slug]
// 2. / (when on org domain e.g. http://calcom.cal.com/. This is through a rewrite from next.config.js)
// Also the getServerSideProps and default export are reused by
// 1. org/[orgSlug]/team/[slug]
// 2. org/[orgSlug]/[user]/[type]
import classNames from "classnames";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import Team from "@components/team/screens/Team";
export { getServerSideProps };
import TeamPage, { type PageProps } from "~/team/team-view";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({
team,
considerUnpublished,
markdownStrippedBio,
isValidOrgDomain,
currentOrgDomain,
}: PageProps) {
useTheme(team.theme);
const routerQuery = useRouterQuery();
const pathname = usePathname();
const showMembers = useToggleQuery("members");
const { t } = useLocale();
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
const teamName = team.name || "Nameless Team";
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
const metadata = teamMetadataSchema.parse(team.metadata);
const Page = (props: PageProps) => <TeamPage {...props} />;
const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);
Page.PageWrapper = PageWrapper;
useEffect(() => {
telemetry.event(
telemetryEventTypes.pageView,
collectPageParameters("/team/[slug]", { isTeamBooking: true })
);
}, [telemetry, pathname]);
if (considerUnpublished) {
const teamSlug = team.slug || metadata?.requestedSlug;
const parentSlug = team.parent?.slug || team.parent?.requestedSlug;
// Show unpublished state for parent Organization itself, if the team is a subteam(team.parent is NOT NULL)
const slugPropertyName = team.parent || team.isOrganization ? "orgSlug" : "teamSlug";
return (
<div className="flex h-full min-h-[calc(100dvh)] items-center justify-center">
<UnpublishedEntity
{...{ [slugPropertyName]: team.parent ? parentSlug : teamSlug }}
logoUrl={team.parent?.logoUrl || team.logoUrl}
name={team.parent ? team.parent.name : team.name}
/>
</div>
);
}
// slug is a route parameter, we don't want to forward it to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
const EventTypes = ({ eventTypes }: { eventTypes: NonNullable<(typeof team)["eventTypes"]> }) => (
<ul className="border-subtle rounded-md border">
{eventTypes.map((type, index) => (
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 bg-default hover:bg-muted border-subtle group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0",
!isEmbed && "bg-default"
)}>
<div className="px-6 py-4 ">
<Link
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}/${type.slug}`,
query: queryParamsToForward,
}}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link"
className="flex justify-between">
<div className="flex-shrink">
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
<h2 className=" text-default text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
<UserAvatarGroup
truncateAfter={4}
className="flex flex-shrink-0"
size="sm"
users={type.users}
/>
</div>
</Link>
</div>
</li>
))}
</ul>
);
const SubTeams = () =>
team.children.length ? (
<ul className="divide-subtle border-subtle bg-default !static w-full divide-y rounded-md border">
{team.children.map((ch, i) => {
const memberCount = team.members.filter(
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
).length;
return (
<li key={i} className="hover:bg-muted w-full rounded-md transition">
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
<div className="flex items-center px-5 py-5">
<div className="ms-3 inline-block truncate">
<span className="text-default text-sm font-bold">{ch.name}</span>
<span className="text-subtle block text-xs">
{t("number_member", {
count: memberCount,
})}
</span>
</div>
</div>
<UserAvatarGroup
className="mr-6"
size="sm"
truncateAfter={4}
users={team.members.filter((mem) => mem.subteams?.includes(ch.slug) && mem.accepted)}
/>
</Link>
</li>
);
})}
</ul>
) : (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-emphasis mb-2 text-3xl">{` ${t("org_no_teams_yet")}`}</h2>
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
</div>
</div>
</div>
);
const profileImageSrc = getOrgOrTeamAvatar(team);
return (
<>
<HeadSeo
origin={getOrgFullOrigin(currentOrgDomain)}
title={teamName}
description={teamName}
meeting={{
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: profileImageSrc,
},
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
)}
</div>
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
</>
)}
</main>
</>
);
}
TeamPage.isBookingPage = true;
TeamPage.PageWrapper = PageWrapper;
export default TeamPage;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+5 -73
View File
@@ -1,78 +1,10 @@
"use client";
import { useSearchParams } from "next/navigation";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
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;
import TypePage, { type PageProps } from "~/team/type-view";
export { getServerSideProps };
const Page = (props: PageProps) => <TypePage {...props} />;
export const getMultipleDurationValue = (
multipleDurationConfig: number[] | undefined,
queryDuration: string | string[] | null | undefined,
defaultValue: number
) => {
if (!multipleDurationConfig) return null;
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
return defaultValue;
};
Page.PageWrapper = PageWrapper;
export default function Type({
slug,
user,
booking,
isEmbed,
isBrandingHidden,
eventData,
isInstantMeeting,
orgBannerUrl,
teamMemberEmail,
}: PageProps) {
const searchParams = useSearchParams();
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
entity={eventData.entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
bookingData={booking}
isInstantMeeting={isInstantMeeting}
hideBranding={isBrandingHidden}
isTeamEvent
entity={eventData.entity}
durationConfig={eventData.metadata?.multipleDuration}
/* TODO: Currently unused, evaluate it is needed-
* Possible alternative approach is to have onDurationChange.
*/
duration={getMultipleDurationValue(
eventData.metadata?.multipleDuration,
searchParams?.get("duration"),
eventData.length
)}
orgBannerUrl={orgBannerUrl}
teamMemberEmail={teamMemberEmail}
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
Type.isBookingPage = true;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
+9 -4
View File
@@ -1,9 +1,14 @@
"use client";
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
import PageWrapper from "@components/PageWrapper";
export { default, type PageProps } from "../[type]";
import TypePage, { type PageProps } from "~/team/type-view";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
const Page = (props: PageProps) => <TypePage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
+9 -4
View File
@@ -1,9 +1,14 @@
"use client";
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../team/[slug]";
import PageWrapper from "@components/PageWrapper";
export { default } from "../../team/[slug]";
import TeamPage, { type PageProps } from "~/team/team-view";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
const Page = (props: PageProps) => <TeamPage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
+6 -41
View File
@@ -1,45 +1,10 @@
"use client";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/teams/teams-view";
import Teams from "~/teams/teams-view";
export { getServerSideProps } from "@lib/teams/getServerSideProps";
function Teams() {
const { t } = useLocale();
const [user] = trpc.viewer.me.useSuspenseQuery();
return (
<Shell
withoutMain={false}
heading={t("teams")}
title="Teams"
description="Create and manage teams to use collaborative features."
hideHeadingOnMobile
subtitle={t("create_manage_teams_collaborative")}
CTA={
(!user.organizationId || user.organization.isOrgAdmin) && (
<Button
data-testid="new-team-btn"
variant="fab"
StartIcon="plus"
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
</Button>
)
}>
<TeamsListing />
</Shell>
);
}
Teams.requiresLicense = false;
Teams.PageWrapper = PageWrapper;
export default Teams;
const Page = (props: PageProps) => <Teams {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
+3
View File
@@ -26,6 +26,9 @@ const searchParams = new URL(document.URL).searchParams;
const only = searchParams.get("only");
const colorScheme = searchParams.get("color-scheme");
const prerender = searchParams.get("prerender");
window.ENABLE_FUTURE_ROUTES = searchParams.get("future-routes") === "true";
if (colorScheme) {
document.documentElement.style.colorScheme = colorScheme;
}
+7
View File
@@ -272,6 +272,13 @@ export class Cal {
// TODO: Make a list of patterns that are embeddable. All except that should be allowed with a warning that "The page isn't optimized for embedding"
urlInstance.pathname = `${urlInstance.pathname}/embed`;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (window.ENABLE_FUTURE_ROUTES) {
urlInstance.pathname = `/future${urlInstance.pathname}`;
}
urlInstance.searchParams.set("embed", this.namespace);
if (config.debug) {
+1 -2
View File
@@ -1,7 +1,6 @@
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import prisma from "@calcom/prisma";
import type { TEventInputSchema } from "../../../trpc/server/routers/publicViewer/event.schema";
import type { TEventInputSchema } from "@calcom/trpc/server/routers/publicViewer/event.schema";
export class EventRepository {
static async getPublicEvent(input: TEventInputSchema) {