Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate the "/org/*" page group #171

Draft
wants to merge 10 commits into
base: migrate/team/page-group
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/web/app/AppDirSSRHOC.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
import { notFound, redirect } from "next/navigation";

export const withAppDir =
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
async (context: GetServerSidePropsContext): Promise<T> => {
const ssrResponse = await getServerSideProps(context);

if ("redirect" in ssrResponse) {
redirect(ssrResponse.redirect.destination);
}

if ("notFound" in ssrResponse) {
notFound();
}

return {
...ssrResponse.props,
// includes dehydratedState required for future page trpcPropvider
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
};
};
22 changes: 22 additions & 0 deletions apps/web/app/future/org/[orgSlug]/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import TeamPage from "@pages/team/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import type { WithLayoutParams } from "app/layoutHOC";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";

export const generateMetadata = async () =>
await _generateMetadata(
// TODO use a simple prisma call to get the organization name
(t) => t("TODO"),
(t) => t("TODO")
);

export default WithLayout({
Page: TeamPage,
getData: withAppDir(withEmbedSsr(getServerSideProps)) as WithLayoutParams<any>["getData"],
getLayout: null,
isBookingPage: true,
})<"P">;
21 changes: 21 additions & 0 deletions apps/web/app/future/org/[orgSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import TeamPage from "@pages/team/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import type { WithLayoutParams } from "app/layoutHOC";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";

export const generateMetadata = async () =>
await _generateMetadata(
// TODO use a simple prisma call to get the organization name
(t) => t("TODO"),
(t) => t("TODO")
);

export default WithLayout({
Page: TeamPage,
getData: withAppDir(getServerSideProps) as WithLayoutParams<any>["getData"],
getLayout: null,
isBookingPage: true,
})<"P">;
21 changes: 21 additions & 0 deletions apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import TypePage from "@pages/team/[slug]/[type]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import type { WithLayoutParams } from "app/layoutHOC";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";

export const generateMetadata = async () =>
await _generateMetadata(
// TODO use a simple prisma call to get the organization name
(t) => t("TODO"),
(t) => t("TODO")
);

export default WithLayout({
Page: TypePage,
getData: withAppDir(getServerSideProps) as WithLayoutParams<any>["getData"],
getLayout: null,
isBookingPage: true,
})<"P">;
21 changes: 21 additions & 0 deletions apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import TeamPage from "@pages/team/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import type { WithLayoutParams } from "app/layoutHOC";
import { WithLayout } from "app/layoutHOC";

import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";

export const generateMetadata = async () =>
await _generateMetadata(
// TODO use a simple prisma call to get the organization name
(t) => t("TODO"),
(t) => t("TODO")
);

export default WithLayout({
Page: TeamPage,
getData: withAppDir(getServerSideProps) as WithLayoutParams<any>["getData"],
getLayout: null,
isBookingPage: true,
})<"P">;
18 changes: 18 additions & 0 deletions apps/web/app/future/team/[slug]/[type]/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getServerSideProps } from "@pages/team/[slug]/[type]";
import { withAppDir } from "app/AppDirSSRHOC";
import type { PageProps } from "app/_types";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";

import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import withEmbedSsr from "@lib/withEmbedSsr";

const Page = async ({ params }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);

await withAppDir(withEmbedSsr(getServerSideProps))(legacyCtx as unknown as GetServerSidePropsContext);

return null;
};

export default Page;
19 changes: 19 additions & 0 deletions apps/web/app/future/team/[slug]/[type]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import LegacyPage, { getServerSideProps as _getServerSideProps } from "@pages/team/[slug]/[type]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";

export const generateMetadata = async () =>
await _generateMetadata(
() => "",
() => ""
);

const getData = withAppDir(_getServerSideProps) as any;

export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
18 changes: 18 additions & 0 deletions apps/web/app/future/team/[slug]/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getServerSideProps } from "@pages/team/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import type { PageProps } from "app/_types";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";

import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import withEmbedSsr from "@lib/withEmbedSsr";

const Page = async ({ params }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);

await withAppDir(withEmbedSsr(getServerSideProps))(legacyCtx as unknown as GetServerSidePropsContext);

return null;
};

export default Page;
30 changes: 30 additions & 0 deletions apps/web/app/future/team/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import LegacyPage, { getServerSideProps as _getServerSideProps } from "@pages/team/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";

import { buildLegacyCtx } from "@lib/buildLegacyCtx";

export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
);
const teamName = props.team.name || "Nameless Team";

return await _generateMetadata(
() => teamName,
() => teamName
);
};

const getData = withAppDir(_getServerSideProps);

export default WithLayout({
Page: LegacyPage,
// @ts-expect-error getData arg
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
30 changes: 25 additions & 5 deletions apps/web/app/layoutHOC.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import type { LayoutProps, PageProps } from "app/_types";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";

import { buildLegacyCtx } from "@lib/buildLegacyCtx";

import PageWrapper from "@components/PageWrapperAppDir";

type WithLayoutParams<T extends Record<string, any>> = {
export type WithLayoutParams<T extends Record<string, any>> = {
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
Page?: (props: T) => React.ReactElement;
getData?: (arg: ReturnType<typeof buildLegacyCtx>) => Promise<T>;
getData?: (arg: GetServerSidePropsContext) => Promise<T>;
isBookingPage?: boolean;
};

export function WithLayout<T extends Record<string, any>>({ getLayout, getData, Page }: WithLayoutParams<T>) {
export function WithLayout<T extends Record<string, any>>({
getLayout,
getData,
Page,
isBookingPage,
}: WithLayoutParams<T>) {
return async <P extends "P" | "L">(p: P extends "P" ? PageProps : LayoutProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const props = getData ? await getData(buildLegacyCtx(h, cookies(), p.params)) : ({} as T);

const searchParams = "searchParams" in p ? p.searchParams : {};

const props = getData
? await getData(
buildLegacyCtx(h, cookies(), p.params, searchParams) as unknown as GetServerSidePropsContext
)
: ({} as T);

const children = "children" in p ? p.children : null;

return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<PageWrapper
getLayout={getLayout}
requiresLicense={false}
nonce={nonce}
themeBasis={null}
isBookingPage={isBookingPage}
{...props}>
{Page ? <Page {...props} /> : children}
</PageWrapper>
);
Expand Down
22 changes: 8 additions & 14 deletions apps/web/lib/buildLegacyCtx.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import type { SearchParams } from "app/_types";
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) => {
export const buildLegacyCtx = (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
params: Params,
searchParams: SearchParams
) => {
return {
query: getQuery(headers.get("x-url") ?? "", params),
query: { ...searchParams, ...params },
params,
req: { headers, cookies },
};
Expand Down
102 changes: 102 additions & 0 deletions apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";

import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } 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 { RedirectType } from "@calcom/prisma/client";

import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";

const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
slug: z.string().transform((s) => slugify(s)),
});

// Booker page fetches a tiny bit of data server side:
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerSession(context);
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
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);
const isOrgContext = currentOrgDomain && isValidOrgDomain;

if (!isOrgContext) {
const redirect = await getTemporaryOrgRedirect({
slug: teamSlug,
redirectType: RedirectType.Team,
eventTypeSlug: meetingSlug,
currentQuery: context.query,
});

if (redirect) {
return redirect;
}
}

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

let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
}

const org = isValidOrgDomain ? currentOrgDomain : null;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username: teamSlug,
eventSlug: meetingSlug,
isTeamEvent: true,
org,
});

if (!eventData) {
return {
notFound: true,
} as const;
}

return {
props: {
entity: eventData.entity,
duration: getMultipleDurationValue(
eventData.metadata?.multipleDuration,
queryDuration,
eventData.length
),
booking,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
themeBasis: null,
},
};
};
Loading
Loading