diff --git a/.env.example b/.env.example index cfb67a88df43d7..c0e3a2bb309fcb 100644 --- a/.env.example +++ b/.env.example @@ -355,6 +355,14 @@ E2E_TEST_OIDC_USER_PASSWORD= # provide a value between 0 and 100 to ensure the percentage of traffic # redirected from the legacy to the future pages AB_TEST_BUCKET_PROBABILITY=50 +APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0 +APP_ROUTER_APPS_SLUG_ENABLED=0 +APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0 +# whether we redirect to the future/apps/categories from /apps/categories or not +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_APPS_ENABLED=0 APP_ROUTER_TEAM_ENABLED=0 APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0 APP_ROUTER_AUTH_LOGIN_ENABLED=0 diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index c103de9fd6231c..27938472378999 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -5,6 +5,11 @@ import { NextResponse, URLPattern } from "next/server"; import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants"; const ROUTES: [URLPattern, boolean][] = [ + ["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const, + ["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const, + ["/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, ["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const, ["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const, ["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const, diff --git a/apps/web/app/_utils.tsx b/apps/web/app/_utils.tsx index 18acce789d7ac1..dd05e0d3aed4f2 100644 --- a/apps/web/app/_utils.tsx +++ b/apps/web/app/_utils.tsx @@ -3,8 +3,8 @@ import i18next from "i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { headers } from "next/headers"; -import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages"; -import { constructGenericImage, constructAppImage, constructMeetingImage } from "@calcom/lib/OgImages"; +import type { MeetingImageProps } from "@calcom/lib/OgImages"; +import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages"; import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants"; import { buildCanonical } from "@calcom/lib/next-seo.config"; import { truncateOnWord } from "@calcom/lib/text"; @@ -62,18 +62,17 @@ const _generateMetadataWithoutImage = async ( const t = await getTranslationWithCache(locale); const title = getTitle(t); - + const description = getDescription(t); const titleSuffix = `| ${APP_NAME}`; const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`; const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL); - const truncatedDescription = truncateOnWord(getDescription(t), 158); return { title: title.length === 0 ? APP_NAME : displayedTitle, - description: truncatedDescription, + description, alternates: { canonical }, openGraph: { - description: truncatedDescription, + description: truncateOnWord(description, 158), url: canonical, type: "website", siteName: APP_NAME, @@ -106,26 +105,6 @@ export const _generateMetadata = async ( }; }; -export const generateAppMetadata = async ( - app: AppImageProps, - getTitle: (t: TFunction) => string, - getDescription: (t: TFunction) => string, - hideBranding?: boolean, - origin?: string -) => { - const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin); - - const image = SEO_IMG_OGIMG + constructAppImage({ ...app, description: metadata.description }); - - return { - ...metadata, - openGraph: { - ...metadata.openGraph, - images: [image], - }, - }; -}; - export const generateMeetingMetadata = async ( meeting: MeetingImageProps, getTitle: (t: TFunction) => string, diff --git a/apps/web/app/apps/[slug]/page.tsx b/apps/web/app/apps/[slug]/page.tsx deleted file mode 100644 index 7cd655d5d3e399..00000000000000 --- a/apps/web/app/apps/[slug]/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { PageProps as _PageProps } from "app/_types"; -import { generateAppMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { notFound } from "next/navigation"; -import { z } from "zod"; - -import { AppRepository } from "@calcom/lib/server/repository/app"; -import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check"; - -import { getStaticProps } from "@lib/apps/[slug]/getStaticProps"; - -import AppView from "~/apps/[slug]/slug-view"; - -const paramsSchema = z.object({ - slug: z.string(), -}); - -export const generateMetadata = async ({ params }: _PageProps) => { - const p = paramsSchema.safeParse(params); - - if (!p.success) { - return notFound(); - } - - const props = await getStaticProps(p.data.slug); - - if (!props) { - notFound(); - } - const { name, logo, description } = props.data; - - return await generateAppMetadata( - { slug: logo, name, description }, - () => name, - () => description - ); -}; - -export const generateStaticParams = async () => { - const isPrismaAvailable = await isPrismaAvailableCheck(); - - if (!isPrismaAvailable) { - // Database is not available at build time. Make sure we fall back to building these pages on demand - return []; - } - const appStore = await AppRepository.findAppStore(); - return appStore.map(({ slug }) => ({ slug })); -}; - -async function Page({ params }: _PageProps) { - const p = paramsSchema.safeParse(params); - - if (!p.success) { - return notFound(); - } - - const props = await getStaticProps(p.data.slug); - - if (!props) { - notFound(); - } - - return ; -} - -export default WithLayout({ getLayout: null, ServerPage: Page }); - -export const dynamic = "force-static"; diff --git a/apps/web/app/apps/categories/[category]/page.tsx b/apps/web/app/apps/categories/[category]/page.tsx deleted file mode 100644 index 0b9b5089c6fb11..00000000000000 --- a/apps/web/app/apps/categories/[category]/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { redirect } from "next/navigation"; -import { z } from "zod"; - -import { AppCategories } from "@calcom/prisma/enums"; - -import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps"; - -import CategoryPage from "~/apps/categories/[category]/category-view"; - -const querySchema = z.object({ - category: z.enum(Object.values(AppCategories) as [AppCategories, ...AppCategories[]]), -}); - -export const generateMetadata = async () => { - return await _generateMetadata( - (t) => t("app_store"), - (t) => t("app_store_description") - ); -}; - -export const generateStaticParams = async () => { - const paths = Object.keys(AppCategories); - return paths.map((category) => ({ category })); -}; - -async function Page({ params, searchParams }: PageProps) { - const parsed = querySchema.safeParse({ ...params, ...searchParams }); - if (!parsed.success) { - redirect("/apps/categories/calendar"); - } - - const props = await getStaticProps(parsed.data.category); - - return ; -} - -export default WithLayout({ getLayout: null, ServerPage: Page }); diff --git a/apps/web/app/apps/installed/[category]/page.tsx b/apps/web/app/apps/installed/[category]/page.tsx deleted file mode 100644 index c8f14c3f4099b0..00000000000000 --- a/apps/web/app/apps/installed/[category]/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { redirect } from "next/navigation"; -import { z } from "zod"; - -import { AppCategories } from "@calcom/prisma/enums"; - -import InstalledApps from "~/apps/installed/[category]/installed-category-view"; - -const querySchema = z.object({ - category: z.nativeEnum(AppCategories), -}); - -export const generateMetadata = async () => { - return await _generateMetadata( - (t) => t("installed_apps"), - (t) => t("manage_your_connected_apps") - ); -}; - -const InstalledAppsWrapper = async ({ params }: PageProps) => { - const parsedParams = querySchema.safeParse(params); - - if (!parsedParams.success) { - redirect("/apps/installed/calendar"); - } - - return ; -}; - -export default WithLayout({ getLayout: null, ServerPage: InstalledAppsWrapper }); diff --git a/apps/web/app/apps/routing-forms/[...pages]/page.tsx b/apps/web/app/apps/routing-forms/[...pages]/page.tsx deleted file mode 100644 index a86b31568d3476..00000000000000 --- a/apps/web/app/apps/routing-forms/[...pages]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "app/routing-forms/[...pages]/page"; diff --git a/apps/web/app/apps/routing-forms/page.tsx b/apps/web/app/apps/routing-forms/page.tsx deleted file mode 100644 index 3abdb6457584d1..00000000000000 --- a/apps/web/app/apps/routing-forms/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "app/routing-forms/page"; diff --git a/apps/web/app/apps/[slug]/[...pages]/page.tsx b/apps/web/app/future/apps/[slug]/[...pages]/page.tsx similarity index 100% rename from apps/web/app/apps/[slug]/[...pages]/page.tsx rename to apps/web/app/future/apps/[slug]/[...pages]/page.tsx diff --git a/apps/web/app/future/apps/[slug]/page.tsx b/apps/web/app/future/apps/[slug]/page.tsx new file mode 100644 index 00000000000000..3c7f717544a24f --- /dev/null +++ b/apps/web/app/future/apps/[slug]/page.tsx @@ -0,0 +1,45 @@ +import { Prisma } from "@prisma/client"; +import { withAppDirSsg } from "app/WithAppDirSsg"; +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 { AppRepository } from "@calcom/lib/server/repository/app"; + +import { getStaticProps } from "@lib/apps/[slug]/getStaticProps"; +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import type { PageProps } from "~/apps/[slug]/slug-view"; +import Page from "~/apps/[slug]/slug-view"; + +const getData = withAppDirSsg(getStaticProps, "future/apps/[slug]"); + +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { + const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams); + const res = await getData(legacyContext); + + return await _generateMetadata( + () => res?.data.name ?? "", + () => res?.data.description ?? "" + ); +}; + +export const generateStaticParams = async () => { + try { + const appStore = await AppRepository.findAppStore(); + return appStore.map(({ slug }) => ({ slug })); + } catch (e: unknown) { + if (e instanceof Prisma.PrismaClientInitializationError) { + // Database is not available at build time, but that's ok – we fall back to resolving paths on demand + } else { + throw e; + } + } + + return []; +}; + +export default WithLayout({ getLayout: null, Page, getData }); + +export const dynamic = "force-static"; diff --git a/apps/web/app/apps/[slug]/setup/page.tsx b/apps/web/app/future/apps/[slug]/setup/page.tsx similarity index 60% rename from apps/web/app/apps/[slug]/setup/page.tsx rename to apps/web/app/future/apps/[slug]/setup/page.tsx index 9689b19e064c19..c10edd12a20c60 100644 --- a/apps/web/app/apps/[slug]/setup/page.tsx +++ b/apps/web/app/future/apps/[slug]/setup/page.tsx @@ -2,34 +2,16 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; -import { notFound } from "next/navigation"; -import { z } from "zod"; import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps"; import Page, { type PageProps } from "~/apps/[slug]/setup/setup-view"; -const paramsSchema = z.object({ - slug: z.string(), -}); - export const generateMetadata = async ({ params }: _PageProps) => { - const p = paramsSchema.safeParse(params); - - if (!p.success) { - return notFound(); - } - const metadata = await _generateMetadata( - () => `${p.data.slug}`, + return await _generateMetadata( + () => `${params.slug}`, () => "" ); - return { - ...metadata, - robots: { - follow: false, - index: false, - }, - }; }; const getData = withAppDirSsr(getServerSideProps); diff --git a/apps/web/app/future/apps/categories/[category]/page.tsx b/apps/web/app/future/apps/categories/[category]/page.tsx new file mode 100644 index 00000000000000..4aae6cb845154d --- /dev/null +++ b/apps/web/app/future/apps/categories/[category]/page.tsx @@ -0,0 +1,34 @@ +import { withAppDirSsg } from "app/WithAppDirSsg"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; + +import { AppCategories } from "@calcom/prisma/enums"; +import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check"; + +import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps"; + +import CategoryPage, { type PageProps } from "~/apps/categories/[category]/category-view"; + +export const generateMetadata = async () => { + return await _generateMetadata( + (t) => t("app_store"), + (t) => t("app_store_description") + ); +}; + +export const generateStaticParams = async () => { + const paths = Object.keys(AppCategories); + const isPrismaAvailable = await isPrismaAvailableCheck(); + + if (!isPrismaAvailable) { + // Database is not available at build time. Make sure we fall back to building these pages on demand + return []; + } + + return paths.map((category) => ({ category })); +}; + +const getData = withAppDirSsg(getStaticProps, "future/apps/categories/[category]"); + +export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">; +export const dynamic = "force-static"; diff --git a/apps/web/app/apps/categories/page.tsx b/apps/web/app/future/apps/categories/page.tsx similarity index 100% rename from apps/web/app/apps/categories/page.tsx rename to apps/web/app/future/apps/categories/page.tsx diff --git a/apps/web/app/apps/installation/[[...step]]/page.tsx b/apps/web/app/future/apps/installation/[[...step]]/page.tsx similarity index 99% rename from apps/web/app/apps/installation/[[...step]]/page.tsx rename to apps/web/app/future/apps/installation/[[...step]]/page.tsx index f1113768203504..650f732d9a3a5d 100644 --- a/apps/web/app/apps/installation/[[...step]]/page.tsx +++ b/apps/web/app/future/apps/installation/[[...step]]/page.tsx @@ -12,6 +12,7 @@ import Page from "~/apps/installation/[[...step]]/step-view"; export const generateMetadata = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); + const { appMetadata } = await getData(legacyCtx); return await _generateMetadata( (t) => `${t("install")} ${appMetadata?.name ?? ""}`, diff --git a/apps/web/app/future/apps/installed/[category]/page.tsx b/apps/web/app/future/apps/installed/[category]/page.tsx new file mode 100644 index 00000000000000..c20bbf0d32fce6 --- /dev/null +++ b/apps/web/app/future/apps/installed/[category]/page.tsx @@ -0,0 +1,18 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; + +import { getServerSidePropsAppDir } from "@lib/apps/installed/[category]/getServerSideProps"; + +import Page from "~/apps/installed/[category]/installed-category-view"; + +export const generateMetadata = async () => { + return await _generateMetadata( + (t) => t("installed_apps"), + (t) => t("manage_your_connected_apps") + ); +}; + +const getData = withAppDirSsr(getServerSidePropsAppDir); + +export default WithLayout({ getLayout: null, getData, Page }); diff --git a/apps/web/app/apps/installed/page.tsx b/apps/web/app/future/apps/installed/page.tsx similarity index 100% rename from apps/web/app/apps/installed/page.tsx rename to apps/web/app/future/apps/installed/page.tsx diff --git a/apps/web/app/routing-forms/[...pages]/page.tsx b/apps/web/app/future/routing-forms/[...pages]/page.tsx similarity index 54% rename from apps/web/app/routing-forms/[...pages]/page.tsx rename to apps/web/app/future/routing-forms/[...pages]/page.tsx index 8bdeb00880167e..4ecc5a1aa0839f 100644 --- a/apps/web/app/routing-forms/[...pages]/page.tsx +++ b/apps/web/app/future/routing-forms/[...pages]/page.tsx @@ -10,10 +10,10 @@ const paramsSchema = z pages: [], }); -const Page = ({ params }: PageProps) => { - const { pages } = paramsSchema.parse(params); +const Page = ({ params, searchParams }: PageProps) => { + const { pages } = paramsSchema.parse({ ...params, ...searchParams }); - redirect(`/routing/${pages.length ? pages.join("/") : ""}`); + redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`); }; export default Page; diff --git a/apps/web/app/routing-forms/page.tsx b/apps/web/app/future/routing-forms/page.tsx similarity index 53% rename from apps/web/app/routing-forms/page.tsx rename to apps/web/app/future/routing-forms/page.tsx index 39c972a38565d8..ba222ba47617af 100644 --- a/apps/web/app/routing-forms/page.tsx +++ b/apps/web/app/future/routing-forms/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; -const Page = async () => { - redirect("/routing/forms"); +const Page = () => { + redirect("/apps/routing-forms/forms"); }; export default Page; diff --git a/apps/web/app/routing/[...pages]/page.tsx b/apps/web/app/routing/[...pages]/page.tsx deleted file mode 100644 index 8f91cfddc58874..00000000000000 --- a/apps/web/app/routing/[...pages]/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import type { GetServerSidePropsResult } from "next"; -import { cookies, headers } from "next/headers"; -import { notFound } from "next/navigation"; -import z from "zod"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/routing/[...pages]/getServerSideProps"; - -import RoutingFormsPage, { getLayout } from "~/routing/[...pages]/pages-view"; - -const paramsSchema = z.object({ - pages: z.array(z.string()), -}); - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const p = paramsSchema.safeParse(params); - - if (!p.success) { - return notFound(); - } - const ctx = buildLegacyCtx(headers(), cookies(), params, searchParams); - const data = await getData({ - ...ctx, - params: { - slug: "routing-forms", - pages: params.pages, - }, - }); - const form = "form" in data ? (data.form as { name?: string; description?: string }) : null; - const formName = form?.name; - const formDescription = form?.description; - - const { pages } = p.data; - - if (pages.includes("routing-link")) { - return await _generateMetadata( - () => `${formName} | Cal.com Forms`, - () => "", - true - ); - } - - return await _generateMetadata( - (t) => formName ?? t("routing_forms"), - (t) => formDescription ?? t("routing_forms_description") - ); -}; - -const getData = withAppDirSsr>(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: PageProps) => { - const ctx = buildLegacyCtx(headers(), cookies(), params, searchParams); - const props = await getData({ - ...ctx, - params: { - slug: "routing-forms", - pages: params.pages, - }, - }); - return ; -}; - -export default WithLayout({ - getLayout, - ServerPage, -}); diff --git a/apps/web/app/routing/page.tsx b/apps/web/app/routing/page.tsx deleted file mode 100644 index 39c972a38565d8..00000000000000 --- a/apps/web/app/routing/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { redirect } from "next/navigation"; - -const Page = async () => { - redirect("/routing/forms"); -}; - -export default Page; diff --git a/apps/web/components/apps/App.tsx b/apps/web/components/apps/App.tsx index f0ef2ead98520b..6ad0c4b60b3da0 100644 --- a/apps/web/components/apps/App.tsx +++ b/apps/web/components/apps/App.tsx @@ -1,6 +1,7 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import Shell from "@calcom/features/shell/Shell"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HeadSeo } from "@calcom/ui"; import type { AppPageProps } from "./AppPage"; import { AppPage } from "./AppPage"; @@ -13,6 +14,11 @@ const ShellHeading = () => { export default function WrappedApp(props: AppPageProps) { return ( } backPath="/apps" withoutSeo> + {props.licenseRequired ? ( diff --git a/apps/web/components/dialog/RerouteDialog.tsx b/apps/web/components/dialog/RerouteDialog.tsx index d39d8686822d41..ca72e40dc83ad2 100644 --- a/apps/web/components/dialog/RerouteDialog.tsx +++ b/apps/web/components/dialog/RerouteDialog.tsx @@ -807,7 +807,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({
{form.name} diff --git a/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts b/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts index ced207cec26f3f..5cd62aade0c127 100644 --- a/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts +++ b/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts @@ -2,6 +2,7 @@ import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; import { z } from "zod"; import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; +import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config"; import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import prisma from "@calcom/prisma"; @@ -31,6 +32,7 @@ type NotFound = { // TODO: It is a candidate for apps.*.generated.* const AppsRouting = { + "routing-forms": RoutingFormsRoutingConfig, typeform: TypeformRoutingConfig, }; diff --git a/apps/web/lib/apps/[slug]/getStaticProps.tsx b/apps/web/lib/apps/[slug]/getStaticProps.tsx index 61079a283492e3..9113ba9c4e5a44 100644 --- a/apps/web/lib/apps/[slug]/getStaticProps.tsx +++ b/apps/web/lib/apps/[slug]/getStaticProps.tsx @@ -1,5 +1,7 @@ import fs from "fs"; import matter from "gray-matter"; +import MarkdownIt from "markdown-it"; +import type { GetStaticPropsContext } from "next"; import path from "path"; import { z } from "zod"; @@ -8,6 +10,8 @@ import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; +const md = new MarkdownIt("default", { html: true, breaks: true }); + export const sourceSchema = z.object({ content: z.string(), data: z.object({ @@ -25,15 +29,15 @@ export const sourceSchema = z.object({ }), }); -export type AppDataProps = NonNullable>>; +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + if (typeof ctx.params?.slug !== "string") return { notFound: true } as const; -export const getStaticProps = async (slug: string) => { const appMeta = await getAppWithMetadata({ - slug, + slug: ctx.params?.slug, }); const appFromDb = await prisma.app.findUnique({ - where: { slug: slug.toLowerCase() }, + where: { slug: ctx.params.slug.toLowerCase() }, }); const isAppAvailableInFileSystem = appMeta; @@ -41,14 +45,16 @@ export const getStaticProps = async (slug: string) => { if (!IS_PRODUCTION && isAppDisabled) { return { - isAppDisabled: true as const, - data: { - ...appMeta, + props: { + isAppDisabled: true as const, + data: { + ...appMeta, + }, }, }; } - if (!appFromDb || !appMeta || isAppDisabled) return null; + if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true } as const; const isTemplate = appMeta.isTemplate; const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName); @@ -79,8 +85,10 @@ export const getStaticProps = async (slug: string) => { }); } return { - isAppDisabled: false as const, - source: { content, data }, - data: appMeta, + props: { + isAppDisabled: false as const, + source: { content, data }, + data: appMeta, + }, }; }; diff --git a/apps/web/lib/apps/categories/[category]/getStaticProps.tsx b/apps/web/lib/apps/categories/[category]/getStaticProps.tsx index 29daa45a820150..7afa898b1c9cfc 100644 --- a/apps/web/lib/apps/categories/[category]/getStaticProps.tsx +++ b/apps/web/lib/apps/categories/[category]/getStaticProps.tsx @@ -1,10 +1,12 @@ +import type { GetStaticPropsContext } from "next"; + import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import prisma from "@calcom/prisma"; import type { AppCategories } from "@calcom/prisma/enums"; -export type CategoryDataProps = NonNullable>>; +export const getStaticProps = async (context: GetStaticPropsContext) => { + const category = context.params?.category as AppCategories; -export const getStaticProps = async (category: AppCategories) => { const appQuery = await prisma.app.findMany({ where: { categories: { @@ -22,7 +24,8 @@ export const getStaticProps = async (category: AppCategories) => { const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug)); return { - apps, - category, + props: { + apps, + }, }; }; diff --git a/apps/web/lib/apps/installed/[category]/getServerSideProps.tsx b/apps/web/lib/apps/installed/[category]/getServerSideProps.tsx new file mode 100644 index 00000000000000..109032a2392980 --- /dev/null +++ b/apps/web/lib/apps/installed/[category]/getServerSideProps.tsx @@ -0,0 +1,78 @@ +import type { GetServerSidePropsContext } from "next"; +import { z } from "zod"; + +import { AppCategories } from "@calcom/prisma/enums"; + +export type querySchemaType = z.infer; + +export const querySchema = z.object({ + category: z.nativeEnum(AppCategories), +}); + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + // get return-to cookie and redirect if needed + const { cookies } = ctx.req; + + const returnTo = cookies["return-to"]; + + if (cookies && returnTo) { + ctx.res.setHeader("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"); + const redirect = { + redirect: { + destination: `${returnTo}`, + permanent: false, + }, + } as const; + + return redirect; + } + + const params = querySchema.safeParse(ctx.params); + + if (!params.success) { + const notFound = { notFound: true } as const; + + return notFound; + } + + return { + props: { + category: params.data.category, + }, + }; +} + +export async function getServerSidePropsAppDir(ctx: GetServerSidePropsContext) { + // get return-to cookie and redirect if needed + const { cookies } = ctx.req; + + const returnTo = cookies["return-to"]; + + if (cookies && returnTo) { + const NextResponse = await import("next/server").then((mod) => mod.NextResponse); + const response = NextResponse.next(); + response.headers.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"); + const redirect = { + redirect: { + destination: `${returnTo}`, + permanent: false, + }, + } as const; + + return redirect; + } + + const params = querySchema.safeParse(ctx.params); + + if (!params.success) { + const notFound = { notFound: true } as const; + + return notFound; + } + + return { + props: { + category: params.data.category, + }, + }; +} diff --git a/apps/web/lib/apps/installed/getServerSideProps.tsx b/apps/web/lib/apps/installed/getServerSideProps.tsx new file mode 100644 index 00000000000000..a43490aea51562 --- /dev/null +++ b/apps/web/lib/apps/installed/getServerSideProps.tsx @@ -0,0 +1,3 @@ +export async function getServerSideProps() { + return { redirect: { permanent: false, destination: "/apps/installed/calendar" } }; +} diff --git a/apps/web/lib/routing/[...pages]/getServerSideProps.ts b/apps/web/lib/routing/[...pages]/getServerSideProps.ts deleted file mode 100644 index 681d51b251a81c..00000000000000 --- a/apps/web/lib/routing/[...pages]/getServerSideProps.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; -import { z } from "zod"; - -import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; -import { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing-server.config"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { prisma } from "@calcom/prisma"; -import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; - -import type { AppProps } from "@lib/app-providers"; - -import { ssrInit } from "@server/lib/ssr"; - -type RoutingPageType = { - getServerSideProps?: AppGetServerSideProps; - // A component than can accept any properties - // eslint-disable-next-line @typescript-eslint/no-explicit-any - default: ((props: any) => JSX.Element) & - Pick; -}; - -type Found = { - notFound: false; - getServerSideProps: RoutingPageType["getServerSideProps"]; -}; - -type NotFound = { - notFound: true; -}; - -function getRoute(pages: string[]) { - const mainPage = pages[0]; - const getServerSideProps = routingServerSidePropsConfig[mainPage] as RoutingPageType["getServerSideProps"]; - - if (!getServerSideProps || typeof getServerSideProps !== "function") { - return { - notFound: true, - } as NotFound; - } - return { notFound: false, getServerSideProps } as Found; -} - -const paramsSchema = z.object({ - slug: z.string(), - pages: z.array(z.string()), -}); - -export async function getServerSideProps( - context: GetServerSidePropsContext -): Promise> { - const { params, req } = context; - if (!params) { - return { - notFound: true, - }; - } - - const parsedParams = paramsSchema.safeParse(params); - if (!parsedParams.success) { - return { - notFound: true, - }; - } - - const appName = parsedParams.data.slug; - const pages = parsedParams.data.pages; - const route = getRoute(pages); - - if (route.notFound) { - return { notFound: true }; - } - - if (route.getServerSideProps) { - params.appPages = pages.slice(1); - const session = await getServerSession({ req }); - const user = session?.user; - const app = await getAppWithMetadata({ slug: appName }); - - if (!app) { - return { - notFound: true, - }; - } - - const result = await route.getServerSideProps( - context as GetServerSidePropsContext<{ - slug: string; - pages: string[]; - appPages: string[]; - }>, - prisma, - user, - ssrInit - ); - - if (result.notFound) { - return { notFound: true }; - } - - if (result.redirect) { - return { redirect: result.redirect }; - } - - return { - props: { - appName, - appUrl: app.simplePath, - ...result.props, - }, - }; - } else { - return { - props: { - appName, - }, - }; - } -} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index e951ebe6dd930c..94f36de3214a88 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -63,9 +63,11 @@ const middleware = async (req: NextRequest): Promise> => { requestHeaders.set("x-csp-enforce", "true"); } - if (url.pathname.startsWith("/apps/installed")) { + if (url.pathname.startsWith("/future/apps/installed")) { const returnTo = req.cookies.get("return-to")?.value; if (returnTo !== undefined) { + requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"); + let validPathname = returnTo; try { @@ -74,14 +76,8 @@ const middleware = async (req: NextRequest): Promise> => { const nextUrl = url.clone(); nextUrl.pathname = validPathname; - - const response = NextResponse.redirect(nextUrl); - response.cookies.set("return-to", "", { - expires: new Date(0), - path: "/", - }); - - return response; + // TODO: Consider using responseWithHeaders here + return NextResponse.redirect(nextUrl, { headers: requestHeaders }); } } @@ -111,10 +107,9 @@ const routingForms = { 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($|\/)/, "/routing/"); + url.pathname = url.pathname.replace(/^\/apps\/routing_forms($|\/)/, "/apps/routing-forms/"); return NextResponse.rewrite(url); } - return null; }, }; @@ -172,11 +167,26 @@ export const config = { "/api/trpc/:path*", "/login", "/auth/login", - "/apps/:path*", - "/getting-started/:path*", - "/workflows/:path*", + "/future/auth/login", + /** + * Paths required by routingForms.handle + */ + "/apps/routing_forms/:path*", + "/event-types/:path*", - "/routing/:path*", + "/apps/installed/:category/", + "/future/apps/installed/:category/", + "/apps/:slug/", + "/future/apps/:slug/", + "/apps/:slug/setup/", + "/future/apps/:slug/setup/", + "/apps/categories/", + "/future/apps/categories/", + "/apps/categories/:category/", + "/future/apps/categories/:category/", + "/workflows/:path*", + "/getting-started/:path*", + "/apps", "/bookings/:path*", "/video/:path*", "/teams/:path*", diff --git a/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx b/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx index 464cd901db244c..1a1ca5d06fba99 100644 --- a/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx +++ b/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx @@ -1,5 +1,6 @@ "use client"; +import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config"; import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; @@ -28,6 +29,7 @@ type NotFound = { // TODO: It is a candidate for apps.*.generated.* const AppsRouting = { + "routing-forms": RoutingFormsRoutingConfig, typeform: TypeformRoutingConfig, }; diff --git a/apps/web/modules/apps/[slug]/setup/setup-view.tsx b/apps/web/modules/apps/[slug]/setup/setup-view.tsx index da07c32b8ef6b0..921423ce79bc20 100644 --- a/apps/web/modules/apps/[slug]/setup/setup-view.tsx +++ b/apps/web/modules/apps/[slug]/setup/setup-view.tsx @@ -7,6 +7,7 @@ import { AppSetupPage } from "@calcom/app-store/_pages/setup"; import type { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import { HeadSeo } from "@calcom/ui"; export type PageProps = inferSSRProps; @@ -27,5 +28,11 @@ export default function SetupInformation(props: PageProps) { router.replace(`/auth/login?${urlSearchParams.toString()}`); } - return ; + return ( + <> + {/* So that the set up page does not get indexed by search engines */} + + + + ); } diff --git a/apps/web/modules/apps/[slug]/slug-view.tsx b/apps/web/modules/apps/[slug]/slug-view.tsx index b0faacc283b98d..6331f81e401735 100644 --- a/apps/web/modules/apps/[slug]/slug-view.tsx +++ b/apps/web/modules/apps/[slug]/slug-view.tsx @@ -1,5 +1,7 @@ "use client"; +import MarkdownIt from "markdown-it"; +import type { InferGetStaticPropsType } from "next"; import Link from "next/link"; import { IS_PRODUCTION } from "@calcom/lib/constants"; @@ -7,12 +9,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { showToast } from "@calcom/ui"; -import type { AppDataProps } from "@lib/apps/[slug]/getStaticProps"; +import type { getStaticProps } from "@lib/apps/[slug]/getStaticProps"; import useRouterQuery from "@lib/hooks/useRouterQuery"; import App from "@components/apps/App"; -function SingleAppPage(props: AppDataProps) { +const md = new MarkdownIt("default", { html: true, breaks: true }); + +export type PageProps = InferGetStaticPropsType; + +function SingleAppPage(props: PageProps) { const { error, setQuery: setError } = useRouterQuery("error"); const { t } = useLocale(); if (error === "account_already_linked") { diff --git a/apps/web/modules/apps/categories/[category]/category-view.tsx b/apps/web/modules/apps/categories/[category]/category-view.tsx index c3d1f7990b9c3c..519dca26dc3665 100644 --- a/apps/web/modules/apps/categories/[category]/category-view.tsx +++ b/apps/web/modules/apps/categories/[category]/category-view.tsx @@ -1,15 +1,20 @@ "use client"; +import type { InferGetStaticPropsType } from "next"; import Link from "next/link"; import Shell from "@calcom/features/shell/Shell"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AppCard, SkeletonText } from "@calcom/ui"; -import type { CategoryDataProps } from "@lib/apps/categories/[category]/getStaticProps"; +import type { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps"; -export default function Apps({ apps, category }: CategoryDataProps) { +export type PageProps = InferGetStaticPropsType; +export default function Apps({ apps }: PageProps) { + const searchParams = useCompatSearchParams(); const { t, isLocaleReady } = useLocale(); + const category = searchParams?.get("category"); return ( <> diff --git a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx index 6245054a032b24..d5937d32518ed9 100644 --- a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx +++ b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx @@ -1,5 +1,6 @@ "use client"; +import Head from "next/head"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; @@ -225,6 +226,12 @@ const OnboardingPage = ({ key={pathname} className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen px-4" data-testid="onboarding"> + + + {t("install")} {appMetadata?.name ?? ""} + + +
diff --git a/apps/web/modules/apps/installed/[category]/installed-category-view.tsx b/apps/web/modules/apps/installed/[category]/installed-category-view.tsx index e59480a6dbb0db..ba392420aa8628 100644 --- a/apps/web/modules/apps/installed/[category]/installed-category-view.tsx +++ b/apps/web/modules/apps/installed/[category]/installed-category-view.tsx @@ -8,9 +8,11 @@ import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/a import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; import type { RemoveAppParams } from "@calcom/features/apps/components/DisconnectIntegrationModal"; import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AppCategories } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import type { Icon } from "@calcom/ui"; import { AppSkeletonLoader as SkeletonLoader, @@ -21,6 +23,7 @@ import { } from "@calcom/ui"; import { QueryCell } from "@lib/QueryCell"; +import type { querySchemaType, getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps"; import { CalendarListContainer } from "@components/apps/CalendarListContainer"; import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout"; @@ -180,13 +183,13 @@ type ModalState = { teamId?: number; }; -type PageProps = { - category: AppCategories; -}; +export type PageProps = inferSSRProps; -export default function InstalledApps({ category }: PageProps) { +export default function InstalledApps(props: PageProps) { + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const utils = trpc.useUtils(); + const category = searchParams?.get("category") as querySchemaType["category"]; const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => { // Exclude calendar and other from categoryList, we handle those slightly differently below return !(category in { other: null, calendar: null }); diff --git a/apps/web/modules/routing/[...pages]/pages-view.tsx b/apps/web/modules/routing/[...pages]/pages-view.tsx deleted file mode 100644 index fe95221a1b004b..00000000000000 --- a/apps/web/modules/routing/[...pages]/pages-view.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; - -import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config"; -import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; -import type { inferSSRProps } from "@calcom/types/inferSSRProps"; - -import type { AppProps } from "@lib/app-providers"; -import type { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps"; - -type RoutingPageType = { - getServerSideProps: AppGetServerSideProps; - // A component than can accept any properties - // eslint-disable-next-line @typescript-eslint/no-explicit-any - default: ((props: any) => JSX.Element) & - Pick; -}; - -type Found = { - notFound: false; - Component: RoutingPageType["default"]; - getServerSideProps: RoutingPageType["getServerSideProps"]; -}; - -type NotFound = { - notFound: true; -}; - -function getRoute(pages: string[]) { - const routingConfig = RoutingFormsRoutingConfig as unknown as Record; - const mainPage = pages[0]; - const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as RoutingPageType); - if (!appPage) { - return { - notFound: true, - } as NotFound; - } - return { notFound: false, Component: appPage.default, ...appPage } as Found; -} -export type PageProps = inferSSRProps; - -const RoutingFormsPage: RoutingPageType["default"] = function RoutingFormsPage(props: PageProps) { - const params = useParams(); - const _pages = params?.pages ?? []; - const pages = Array.isArray(_pages) ? _pages : _pages.split("/"); - const route = getRoute(pages); - - const componentProps = { - ...props, - pages: pages.slice(1), - }; - - if (!route || route.notFound) { - throw new Error("Route can't be undefined"); - } - return ; -}; - -export const getLayout: NonNullable<(typeof RoutingFormsPage)["getLayout"]> = (page) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const params = useParams(); - const _pages = params?.pages ?? []; - const pages = Array.isArray(_pages) ? _pages : _pages.split("/"); - const route = getRoute(pages as string[]); - - if (route.notFound) { - return null; - } - if (!route.Component.getLayout) { - return page; - } - return route.Component.getLayout(page); -}; - -export default RoutingFormsPage; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index a5e69f50ef7f8f..f66a47cedf416d 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -295,7 +295,7 @@ const nextConfig = { const beforeFiles = [ { source: "/forms/:formQuery*", - destination: "/routing/routing-link/:formQuery*", + destination: "/apps/routing-forms/routing-link/:formQuery*", }, { source: "/success/:path*", diff --git a/apps/web/pages/apps/[slug]/[...pages].tsx b/apps/web/pages/apps/[slug]/[...pages].tsx new file mode 100644 index 00000000000000..6de4cdfd96f81a --- /dev/null +++ b/apps/web/pages/apps/[slug]/[...pages].tsx @@ -0,0 +1,15 @@ +import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps"; + +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/[slug]/[...pages]/pages-view"; +import PagesView, { getLayout } from "~/apps/[slug]/[...pages]/pages-view"; + +const Page = (props: PageProps) => ; + +Page.PageWrapper = PageWrapper; +Page.getLayout = getLayout; + +export { getServerSideProps }; + +export default Page; diff --git a/apps/web/pages/apps/[slug]/index.tsx b/apps/web/pages/apps/[slug]/index.tsx new file mode 100644 index 00000000000000..b3419ceb9b9e0f --- /dev/null +++ b/apps/web/pages/apps/[slug]/index.tsx @@ -0,0 +1,39 @@ +import { Prisma } from "@prisma/client"; +import type { GetStaticPaths } from "next"; + +import { AppRepository } from "@calcom/lib/server/repository/app"; + +import { getStaticProps } from "@lib/apps/[slug]/getStaticProps"; + +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/[slug]/slug-view"; +import SingleAppPage from "~/apps/[slug]/slug-view"; + +const Page = (props: PageProps) => ; + +export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => { + let paths: { params: { slug: string } }[] = []; + + try { + const appStore = await AppRepository.findAppStore(); + paths = appStore.map(({ slug }) => ({ params: { slug } })); + } catch (e: unknown) { + if (e instanceof Prisma.PrismaClientInitializationError) { + // Database is not available at build time, but that's ok – we fall back to resolving paths on demand + } else { + throw e; + } + } + + return { + paths, + fallback: "blocking", + }; +}; + +export { getStaticProps }; + +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/apps/[slug]/setup.tsx b/apps/web/pages/apps/[slug]/setup.tsx new file mode 100644 index 00000000000000..625040aa7aa47b --- /dev/null +++ b/apps/web/pages/apps/[slug]/setup.tsx @@ -0,0 +1,14 @@ +import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps"; + +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/[slug]/setup/setup-view"; +import SetupView from "~/apps/[slug]/setup/setup-view"; + +const Page = (props: PageProps) => ; + +Page.PageWrapper = PageWrapper; + +export { getServerSideProps }; + +export default Page; diff --git a/apps/web/pages/apps/categories/[category].tsx b/apps/web/pages/apps/categories/[category].tsx new file mode 100644 index 00000000000000..7c1a1a39d6cb5a --- /dev/null +++ b/apps/web/pages/apps/categories/[category].tsx @@ -0,0 +1,33 @@ +import { AppCategories } from "@calcom/prisma/enums"; +import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check"; + +import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps"; + +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/categories/[category]/category-view"; +import CategoryView from "~/apps/categories/[category]/category-view"; + +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; + +export const getStaticPaths = async () => { + const paths = Object.keys(AppCategories); + const isPrismaAvailable = await isPrismaAvailableCheck(); + if (!isPrismaAvailable) { + // Database is not available at build time. Make sure we fall back to building these pages on demand + return { + paths: [], + fallback: "blocking", + }; + } + + return { + paths: paths.map((category) => ({ params: { category } })), + fallback: false, + }; +}; + +export default Page; + +export { getStaticProps }; diff --git a/apps/web/pages/apps/categories/index.tsx b/apps/web/pages/apps/categories/index.tsx new file mode 100644 index 00000000000000..54c738a5c9c7b3 --- /dev/null +++ b/apps/web/pages/apps/categories/index.tsx @@ -0,0 +1,12 @@ +import { getServerSideProps } from "@lib/apps/categories/getServerSideProps"; + +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/categories/categories-view"; +import Apps from "~/apps/categories/categories-view"; + +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; + +export default Page; +export { getServerSideProps }; diff --git a/apps/web/pages/apps/installation/[[...step]].tsx b/apps/web/pages/apps/installation/[[...step]].tsx new file mode 100644 index 00000000000000..d50152f1be94b4 --- /dev/null +++ b/apps/web/pages/apps/installation/[[...step]].tsx @@ -0,0 +1,12 @@ +import PageWrapper from "@components/PageWrapper"; + +import type { OnboardingPageProps } from "~/apps/installation/[[...step]]/step-view"; +import StepView from "~/apps/installation/[[...step]]/step-view"; + +const Page = (props: OnboardingPageProps) => ; + +Page.PageWrapper = PageWrapper; + +export { getServerSideProps } from "@lib/apps/installation/[[...step]]/getServerSideProps"; + +export default Page; diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx new file mode 100644 index 00000000000000..7a8e670ce1efea --- /dev/null +++ b/apps/web/pages/apps/installed/[category].tsx @@ -0,0 +1,12 @@ +import PageWrapper from "@components/PageWrapper"; + +import type { PageProps } from "~/apps/installed/[category]/installed-category-view"; +import InstalledApps from "~/apps/installed/[category]/installed-category-view"; + +const Page = (props: PageProps) => ; + +Page.PageWrapper = PageWrapper; + +export { getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps"; + +export default Page; diff --git a/apps/web/pages/apps/installed/index.tsx b/apps/web/pages/apps/installed/index.tsx new file mode 100644 index 00000000000000..02786d55a89e1e --- /dev/null +++ b/apps/web/pages/apps/installed/index.tsx @@ -0,0 +1,7 @@ +export { getServerSideProps } from "@lib/apps/installed/getServerSideProps"; + +function RedirectPage() { + return; +} + +export default RedirectPage; diff --git a/apps/web/pages/routing-forms/[...pages].tsx b/apps/web/pages/routing-forms/[...pages].tsx new file mode 100644 index 00000000000000..13576b185aa739 --- /dev/null +++ b/apps/web/pages/routing-forms/[...pages].tsx @@ -0,0 +1,25 @@ +import type { GetServerSidePropsContext } from "next"; +import z from "zod"; + +const paramsSchema = z + .object({ + pages: z.array(z.string()), + }) + .catch({ + pages: [], + }); + +export default function RoutingForms() { + return null; +} + +export const getServerSideProps = (context: GetServerSidePropsContext) => { + const { pages } = paramsSchema.parse(context.params); + + return { + redirect: { + destination: `/apps/routing-forms/${pages.length ? pages.join("/") : ""}`, + permanent: false, + }, + }; +}; diff --git a/apps/web/pages/routing-forms/index.tsx b/apps/web/pages/routing-forms/index.tsx new file mode 100644 index 00000000000000..c2032de1ddc1d5 --- /dev/null +++ b/apps/web/pages/routing-forms/index.tsx @@ -0,0 +1,12 @@ +export default function RoutingFormsIndex() { + return null; +} + +export const getServerSideProps = () => { + return { + redirect: { + destination: `/apps/routing-forms/forms`, + permanent: false, + }, + }; +}; diff --git a/apps/web/public/icons/sprite.svg b/apps/web/public/icons/sprite.svg index 23e31196942e56..36cf696c717605 100644 --- a/apps/web/public/icons/sprite.svg +++ b/apps/web/public/icons/sprite.svg @@ -570,12 +570,6 @@ - - - - - - diff --git a/apps/web/scripts/vercel-app-router-deploy.sh b/apps/web/scripts/vercel-app-router-deploy.sh index 8f1a6ab423e318..ec1256da7fac8a 100755 --- a/apps/web/scripts/vercel-app-router-deploy.sh +++ b/apps/web/scripts/vercel-app-router-deploy.sh @@ -6,6 +6,11 @@ checkRoute () { # These conditionals are used to remove directories from the build that are not needed in production # This is to reduce the size of the build and prevent OOM errors +checkRoute "$APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED" app/future/apps/installed +checkRoute "$APP_ROUTER_APPS_SLUG_ENABLED" app/future/apps/\[slug\] +checkRoute "$APP_ROUTER_APPS_SLUG_SETUP_ENABLED" app/future/apps/\[slug\]/setup +checkRoute "$APP_ROUTER_APPS_CATEGORIES_ENABLED" app/future/apps/categories +checkRoute "$APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED" app/future/apps/categories/\[category\] checkRoute "$APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED" app/future/auth/forgot-password checkRoute "$APP_ROUTER_AUTH_LOGIN_ENABLED" app/future/auth/login checkRoute "$APP_ROUTER_AUTH_LOGOUT_ENABLED" app/future/auth/logout diff --git a/packages/app-store/routing-forms/api/add.ts b/packages/app-store/routing-forms/api/add.ts index 2b9eac638f096f..5037d705716135 100644 --- a/packages/app-store/routing-forms/api/add.ts +++ b/packages/app-store/routing-forms/api/add.ts @@ -20,7 +20,7 @@ const handler: AppDeclarativeHandler = { }); }, redirect: { - url: "/routing/forms", + url: "/apps/routing-forms/forms", }, }; diff --git a/packages/app-store/routing-forms/config.json b/packages/app-store/routing-forms/config.json index 60e420affe1971..22eacf80976013 100644 --- a/packages/app-store/routing-forms/config.json +++ b/packages/app-store/routing-forms/config.json @@ -10,7 +10,7 @@ "variant": "other", "categories": ["automation"], "publisher": "Cal.com, Inc.", - "simplePath": "/routing", + "simplePath": "/apps/routing-forms", "email": "help@cal.com", "licenseRequired": true, "teamsPlanRequired": { diff --git a/packages/app-store/routing-forms/pages/app-routing-server.config.ts b/packages/app-store/routing-forms/pages/app-routing-server.config.ts deleted file mode 100644 index a80ce55813d5e8..00000000000000 --- a/packages/app-store/routing-forms/pages/app-routing-server.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; - -import { getServerSidePropsForSingleFormView as getServerSidePropsSingleForm } from "../components/getServerSidePropsSingleForm"; -import { getServerSideProps as getServerSidePropsForms } from "./forms/getServerSideProps"; -import { getServerSideProps as getServerSidePropsRoutingLink } from "./routing-link/getServerSideProps"; - -export const routingServerSidePropsConfig: Record = { - forms: getServerSidePropsForms, - "form-edit": getServerSidePropsSingleForm, - "route-builder": getServerSidePropsSingleForm, - "routing-link": getServerSidePropsRoutingLink, - reporting: getServerSidePropsSingleForm, - "incomplete-booking": getServerSidePropsSingleForm, -}; diff --git a/packages/app-store/routing-forms/pages/app-routing.config.tsx b/packages/app-store/routing-forms/pages/app-routing.config.tsx index f54eb9d1bef8a1..4c322f1c21c4aa 100644 --- a/packages/app-store/routing-forms/pages/app-routing.config.tsx +++ b/packages/app-store/routing-forms/pages/app-routing.config.tsx @@ -1,11 +1,17 @@ //TODO: Generate this file automatically so that like in Next.js file based routing can work automatically +import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; + +import { getServerSidePropsForSingleFormView as getServerSidePropsSingleForm } from "../components/getServerSidePropsSingleForm"; import * as formEdit from "./form-edit/[...appPages]"; import * as forms from "./forms/[...appPages]"; +// extracts getServerSideProps function from the client component +import { getServerSideProps as getServerSidePropsForms } from "./forms/getServerSideProps"; import * as IncompleteBooking from "./incomplete-booking/[...appPages]"; import * as LayoutHandler from "./layout-handler/[...appPages]"; import * as Reporting from "./reporting/[...appPages]"; import * as RouteBuilder from "./route-builder/[...appPages]"; import * as RoutingLink from "./routing-link/[...appPages]"; +import { getServerSideProps as getServerSidePropsRoutingLink } from "./routing-link/getServerSideProps"; const routingConfig = { "form-edit": formEdit, @@ -17,4 +23,13 @@ const routingConfig = { "incomplete-booking": IncompleteBooking, }; +export const serverSidePropsConfig: Record = { + forms: getServerSidePropsForms, + "form-edit": getServerSidePropsSingleForm, + "route-builder": getServerSidePropsSingleForm, + "routing-link": getServerSidePropsRoutingLink, + reporting: getServerSidePropsSingleForm, + "incomplete-booking": getServerSidePropsSingleForm, +}; + export default routingConfig; diff --git a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx b/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx index b164fcd0710c17..2aa2e63f43188a 100644 --- a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx @@ -481,5 +481,9 @@ export default function FormEditPage({ } FormEditPage.getLayout = (page: React.ReactElement) => { - return {page}; + return ( + + {page} + + ); }; diff --git a/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx b/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx index cda51e3a984831..0bad763f40d85b 100644 --- a/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState, useEffect } from "react"; import type z from "zod"; diff --git a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx b/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx index 4838b4a1674279..855cdeef798a5e 100644 --- a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx @@ -293,5 +293,9 @@ export default function ReporterWrapper({ } ReporterWrapper.getLayout = (page: React.ReactElement) => { - return {page}; + return ( + + {page} + + ); }; diff --git a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx b/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx index 068fb20522e172..d3f6935295d48f 100644 --- a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx @@ -1092,7 +1092,11 @@ export default function RouteBuilder({ } RouteBuilder.getLayout = (page: React.ReactElement) => { - return {page}; + return ( + + {page} + + ); }; export { getServerSideProps }; diff --git a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx index 2a6661a77e5f53..17b5a572dd2d5b 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx @@ -1,5 +1,6 @@ "use client"; +import Head from "next/head"; import { useRouter } from "next/navigation"; import type { FormEvent } from "react"; import { useEffect, useRef, useState } from "react"; @@ -159,6 +160,9 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
{!customPageMessage ? ( <> + + {`${form.name} | Cal.com Forms`} +
diff --git a/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts b/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts index f17ef544de1608..a0671d861e14d4 100644 --- a/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts +++ b/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts @@ -24,6 +24,9 @@ export const getServerSideProps = async function getServerSideProps( const { currentOrgDomain } = orgDomainConfig(context.req); const isEmbed = params.appPages[1] === "embed"; + if (context.query["flag.coep"] === "true") { + context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + } const form = await prisma.app_RoutingForms_Form.findFirst({ where: { diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts index 0599c12bb4f46d..ce421eee545ff4 100644 --- a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts @@ -93,7 +93,7 @@ test.describe("Routing Forms", () => { await gotoRoutingLink({ page, formId }); await expect(page.locator("text=Test Form Name")).toBeVisible(); - await page.goto(`routing/route-builder/${formId}`); + await page.goto(`apps/routing-forms/route-builder/${formId}`); await disableForm(page); await gotoRoutingLink({ page, formId }); await expect(page.getByTestId(`404-page`)).toBeVisible(); @@ -101,12 +101,12 @@ test.describe("Routing Forms", () => { test("recently added form appears first in the list", async ({ page }) => { await addForm(page, { name: "Test Form 1" }); - await page.goto(`routing/forms`); + await page.goto(`apps/routing-forms/forms`); await page.waitForSelector('[data-testid="routing-forms-list"]'); await expect(page.locator('[data-testid="routing-forms-list"] > div h1')).toHaveCount(1); await addForm(page, { name: "Test Form 2" }); - await page.goto(`routing/forms`); + await page.goto(`apps/routing-forms/forms`); await page.waitForSelector('[data-testid="routing-forms-list"]'); await expect(page.locator('[data-testid="routing-forms-list"] > div h1')).toHaveCount(2); @@ -351,7 +351,7 @@ test.describe("Routing Forms", () => { ["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", "", "", expect.any(String)], ]); - await page.goto(`routing/route-builder/${routingForm.id}`); + await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`); const [download] = await Promise.all([ // Start waiting for the download page.waitForEvent("download"), @@ -446,7 +446,7 @@ test.describe("Routing Forms", () => { test("Test preview should return correct route", async ({ page, users }) => { const user = await createUserAndLogin({ users, page }); const routingForm = user.routingForms[0]; - await page.goto(`routing/form-edit/${routingForm.id}`); + await page.goto(`apps/routing-forms/form-edit/${routingForm.id}`); await page.click('[data-testid="test-preview"]'); //event redirect @@ -696,7 +696,7 @@ async function addAllTypesOfFieldsAndSaveForm( page: Page, form: { description: string; label: string } ) { - await page.goto(`routing/form-edit/${formId}`); + await page.goto(`apps/routing-forms/form-edit/${formId}`); await page.click('[data-testid="add-field"]'); await page.fill('[data-testid="description"]', form.description); @@ -752,7 +752,7 @@ async function addAllTypesOfFieldsAndSaveForm( } async function addShortTextFieldAndSaveForm({ page, formId }: { page: Page; formId: string }) { - await page.goto(`routing/form-edit/${formId}`); + await page.goto(`apps/routing-forms/form-edit/${formId}`); await page.click('[data-testid="add-field"]'); await page.locator(".data-testid-field-type").nth(0).click(); await page.fill(`[name="fields.0.label"]`, "Short Text"); diff --git a/packages/app-store/routing-forms/playwright/tests/testUtils.ts b/packages/app-store/routing-forms/playwright/tests/testUtils.ts index 1c576978d4690c..7f86e19721c081 100644 --- a/packages/app-store/routing-forms/playwright/tests/testUtils.ts +++ b/packages/app-store/routing-forms/playwright/tests/testUtils.ts @@ -36,7 +36,7 @@ export async function addOneFieldAndDescriptionAndSaveForm( page: Page, form: { description?: string; field?: { typeIndex: number; label: string } } ) { - await page.goto(`routing/form-edit/${formId}`); + await page.goto(`apps/routing-forms/form-edit/${formId}`); await page.click('[data-testid="add-field"]'); if (form.description) { await page.fill('[data-testid="description"]', form.description); diff --git a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx index ac486b340fc002..c4fd176dc38dff 100644 --- a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx +++ b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx @@ -17,7 +17,7 @@ export default function HowToUse() {
  1. Make sure that you have{" "} - + Routing Forms {" "} app installed diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 2dc9184482132b..4094e515e3031b 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -25,7 +25,7 @@ import { useAppTheme } from "./useAppTheme"; const Layout = (props: LayoutProps) => { const { banners, bannersHeight } = useBanners(); const pathname = usePathname(); - const isFullPageWithoutSidebar = pathname?.startsWith("/routing/reporting/"); + const isFullPageWithoutSidebar = pathname?.startsWith("/apps/routing-forms/reporting/"); const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title; const withoutSeo = props.withoutSeo ?? props.withoutMain ?? false; @@ -138,26 +138,23 @@ export function ShellMain(props: LayoutProps) { const { buttonToShow, isLoading, enableNotifications, disableNotifications } = useNotifications(); - const backPath = props.backPath; - // Replace multiple leading slashes with a single slash - // e.g., `//routing/forms` -> `/routing/forms` - const validBackPath = typeof backPath === "string" ? backPath.replace(/^\/+/, "/") : null; - return ( <> - {(props.heading || !!backPath) && ( + {(props.heading || !!props.backPath) && (
    - {!!backPath && ( + {!!props.backPath && (