diff --git a/apps/web/app/future/apps/[slug]/[...pages]/page.tsx b/apps/web/app/future/apps/[slug]/[...pages]/page.tsx new file mode 100644 index 0000000000..a1220f7bd8 --- /dev/null +++ b/apps/web/app/future/apps/[slug]/[...pages]/page.tsx @@ -0,0 +1,197 @@ +import LegacyPage, { getLayout } from "@pages/apps/[slug]/[...pages]"; +import type { PageProps, SearchParams } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import type { GetServerSidePropsContext } from "next"; +import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; +import { cookies, headers } from "next/headers"; +import { notFound, redirect } from "next/navigation"; +import z from "zod"; + +import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; +import RoutingFormsRoutingConfig, { + serverSidePropsConfig, +} 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 { APP_NAME } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; +import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; + +import type { AppProps } from "@lib/app-providers"; +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +import { ssrInit } from "@server/lib/ssr"; + +type AppPageType = { + 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: AppPageType["default"]; + getServerSideProps: AppPageType["getServerSideProps"]; +}; + +const AppsRouting = { + "routing-forms": RoutingFormsRoutingConfig, + typeform: TypeformRoutingConfig, +}; + +const paramsSchema = z.object({ + slug: z.string(), + pages: z.array(z.string()), +}); + +export const generateMetadata = async ({ + params, + searchParams, +}: { + params: Params; + searchParams: SearchParams; +}) => { + const p = paramsSchema.safeParse(params); + + if (!p.success) { + return notFound(); + } + + const mainPage = p.data.pages[0]; + + if (mainPage === "forms") { + return await _generateMetadata( + () => `Forms | ${APP_NAME}`, + () => "" + ); + } + + const legacyContext = buildLegacyCtx( + headers(), + cookies(), + params, + searchParams + ) as unknown as GetServerSidePropsContext; + const { form } = await getPageProps(legacyContext); + + return await _generateMetadata( + () => `${form.name} | ${APP_NAME}`, + () => form.description + ); +}; + +function getRoute(appName: string, pages: string[]) { + const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record; + + if (!routingConfig) { + notFound(); + } + + const mainPage = pages[0]; + const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType); + + const getServerSidePropsHandler = serverSidePropsConfig[mainPage]; + + if (!appPage) { + notFound(); + } + + return { + notFound: false, + Component: appPage.default, + ...appPage, + getServerSideProps: getServerSidePropsHandler, + } as Found; +} + +const getPageProps = async ({ params, query, req }: GetServerSidePropsContext) => { + const p = paramsSchema.safeParse(params); + + if (!p.success) { + return notFound(); + } + + const { slug: appName, pages } = p.data; + + const route = getRoute(appName, pages); + + if (route.notFound) { + return route; + } + + if (route.getServerSideProps) { + // TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name. + // appPages is actually hardcoded here and no matter the fileName the same variable would be used. + // We can write some validation logic later on that ensures that [...appPages].tsx file exists + params!.appPages = pages.slice(1); + + const ctx = { req, params, query }; + + const session = await getServerSession({ req }); + const user = session?.user; + const app = await getAppWithMetadata({ slug: appName }); + + if (!app) { + notFound(); + } + + const result = await route.getServerSideProps( + { + ...ctx, + params: { + ...ctx.params, + appPages: pages.slice(1), + }, + } as GetServerSidePropsContext<{ + slug: string; + pages: string[]; + appPages: string[]; + }>, + prisma, + user, + ssrInit + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + + if (result.notFound) { + notFound(); + } + + if (result.redirect) { + redirect(result.redirect.destination); + } + + return { + appName, + appUrl: app.simplePath || `/apps/${appName}`, + ...result.props, + }; + } else { + return { + appName, + }; + } +}; + +export default async function Page({ params, searchParams }: PageProps) { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + const legacyContext = buildLegacyCtx( + h, + cookies(), + params, + searchParams + ) as unknown as GetServerSidePropsContext; + const props = await getPageProps(legacyContext); + return ( + + + + ); +} diff --git a/apps/web/app/future/routing-forms/[...pages]/page.tsx b/apps/web/app/future/routing-forms/[...pages]/page.tsx new file mode 100644 index 0000000000..3ee4fbd90d --- /dev/null +++ b/apps/web/app/future/routing-forms/[...pages]/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from "next/navigation"; + +const getPageProps = () => { + return redirect(`/apps/routing-forms/forms`); +}; +const Page = () => { + getPageProps(); + + return null; +}; + +export default Page; diff --git a/apps/web/app/future/routing-forms/page.tsx b/apps/web/app/future/routing-forms/page.tsx new file mode 100644 index 0000000000..eb67e8796c --- /dev/null +++ b/apps/web/app/future/routing-forms/page.tsx @@ -0,0 +1,36 @@ +import { type SearchParams } from "app/_types"; +import { type GetServerSidePropsContext } from "next"; +import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; +import { cookies, headers } from "next/headers"; +import { redirect } from "next/navigation"; +import z from "zod"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +const paramsSchema = z + .object({ + pages: z.array(z.string()), + }) + .catch({ + pages: [], + }); + +const getPageProps = async (context: GetServerSidePropsContext) => { + const { pages } = paramsSchema.parse(context.params); + + return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`); +}; + +type PageProps = Readonly<{ + params: Params; + searchParams: SearchParams; +}>; + +const Page = async ({ params, searchParams }: PageProps) => { + const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); + await getPageProps(legacyCtx as unknown as GetServerSidePropsContext); + + return null; +}; + +export default Page; diff --git a/apps/web/components/PageWrapper.tsx b/apps/web/components/PageWrapper.tsx index bdd311d2a9..037cbb921c 100644 --- a/apps/web/components/PageWrapper.tsx +++ b/apps/web/components/PageWrapper.tsx @@ -89,8 +89,7 @@ function PageWrapper(props: AppProps) { ) : ( - ), - router + ) )} ); diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index a7bfcba9ea..ae969da583 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -46,7 +46,7 @@ export type AppProps = Omit< requiresLicense?: boolean; isThemeSupported?: boolean; isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean); - getLayout?: (page: React.ReactElement, router: NextAppProps["router"]) => ReactNode; + getLayout?: (page: React.ReactElement) => ReactNode; PageWrapper?: (props: AppProps) => JSX.Element; }; diff --git a/apps/web/lib/getQuery.tsx b/apps/web/lib/getQuery.tsx new file mode 100644 index 0000000000..dfa8d26755 --- /dev/null +++ b/apps/web/lib/getQuery.tsx @@ -0,0 +1,11 @@ +// returns query object same as ctx.query but for app dir +export const getQuery = (url: string, params: Record) => { + if (!url.length) { + return params; + } + + const { searchParams } = new URL(url); + const searchParamsObj = Object.fromEntries(searchParams.entries()); + + return { ...searchParamsObj, ...params }; +}; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 213869dbfa..2b2cc8d010 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -116,6 +116,7 @@ export const config = { * Paths required by routingForms.handle */ "/apps/routing_forms/:path*", + "/event-types", "/future/event-types/", "/settings/admin/:path*", diff --git a/apps/web/pages/apps/[slug]/[...pages].tsx b/apps/web/pages/apps/[slug]/[...pages].tsx index 9729904aff..82aab0a669 100644 --- a/apps/web/pages/apps/[slug]/[...pages].tsx +++ b/apps/web/pages/apps/[slug]/[...pages].tsx @@ -1,3 +1,5 @@ +"use client"; + import type { GetServerSidePropsContext } from "next"; import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; @@ -48,7 +50,6 @@ function getRoute(appName: string, pages: string[]) { } const mainPage = pages[0]; const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType); - if (!appPage) { return { notFound: true, @@ -60,7 +61,7 @@ function getRoute(appName: string, pages: string[]) { const AppPage: AppPageType["default"] = function AppPage(props) { const appName = props.appName; const params = useParamsWithFallback(); - const pages = (params.pages || []) as string[]; + const pages = Array.isArray(params.pages) ? params.pages : params.pages?.split("/") ?? []; const route = getRoute(appName, pages); const componentProps = { @@ -87,18 +88,22 @@ AppPage.isBookingPage = ({ router }) => { return !!isBookingPage; }; -AppPage.getLayout = (page, router) => { - const route = getRoute(router.query.slug as string, router.query.pages as string[]); +export const getLayout: NonNullable<(typeof AppPage)["getLayout"]> = (page) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { slug, pages } = useParamsWithFallback(); + const route = getRoute(slug as string, pages as string[]); + if (route.notFound) { return null; } if (!route.Component.getLayout) { return page; } - return route.Component.getLayout(page, router); + return route.Component.getLayout(page); }; AppPage.PageWrapper = PageWrapper; +AppPage.getLayout = getLayout; export default AppPage; diff --git a/apps/web/pages/settings/organizations/[id]/add-teams.tsx b/apps/web/pages/settings/organizations/[id]/add-teams.tsx index 070d7e6d49..450614e07a 100644 --- a/apps/web/pages/settings/organizations/[id]/add-teams.tsx +++ b/apps/web/pages/settings/organizations/[id]/add-teams.tsx @@ -1,7 +1,6 @@ "use client"; -import type { AppProps as NextAppProps } from "next/app"; -import { redirect } from "next/navigation"; +import { redirect, useRouter } from "next/navigation"; import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -22,8 +21,11 @@ const AddNewTeamsPage = () => { ); }; -AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => ( - <> +AddNewTeamsPage.getLayout = (page: React.ReactElement) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const router = useRouter(); + + return ( {page} - -); + ); +}; AddNewTeamsPage.PageWrapper = PageWrapper; diff --git a/apps/web/pages/settings/organizations/[id]/onboard-admins.tsx b/apps/web/pages/settings/organizations/[id]/onboard-admins.tsx index ee42522c78..34628adc0b 100644 --- a/apps/web/pages/settings/organizations/[id]/onboard-admins.tsx +++ b/apps/web/pages/settings/organizations/[id]/onboard-admins.tsx @@ -1,9 +1,9 @@ "use client"; -import type { AppProps as NextAppProps } from "next/app"; -import { redirect } from "next/navigation"; +import { redirect, useRouter } from "next/navigation"; import { AddNewOrgAdminsForm } from "@calcom/features/ee/organizations/components"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui"; @@ -25,16 +25,23 @@ const OnboardTeamMembersPage = () => { ); }; -OnboardTeamMembersPage.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => ( - { - router.push(`/settings/organizations/${router.query.id}/add-teams`); - }}> - {page} - -); +OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const router = useRouter(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const query = useCompatSearchParams(); + + return ( + { + router.push(`/settings/organizations/${query.get("id")}/add-teams`); + }}> + {page} + + ); +}; export const buildWrappedOnboardTeamMembersPage = ( id: string | string[] | undefined, diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 2c965fffee..32c9909344 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -9,12 +9,6 @@ import { ShellMain } from "@calcom/features/shell/Shell"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import type { - AppGetServerSidePropsContext, - AppPrisma, - AppSsrInit, - AppUser, -} from "@calcom/types/AppGetServerSideProps"; import { Alert, Badge, @@ -45,13 +39,13 @@ import { } from "@calcom/ui/components/icon"; import { RoutingPages } from "../lib/RoutingPages"; -import { getSerializableForm } from "../lib/getSerializableForm"; import { isFallbackRoute } from "../lib/isFallbackRoute"; import { processRoute } from "../lib/processRoute"; import type { Response, Route, SerializableForm } from "../types/types"; import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions"; import FormInputFields from "./FormInputFields"; import RoutingNavBar from "./RoutingNavBar"; +import { getServerSidePropsForSingleFormView } from "./getServerSidePropsSingleForm"; type RoutingForm = SerializableForm; @@ -548,70 +542,4 @@ export default function SingleFormWrapper({ form: _form, ...props }: SingleFormC ); } -export const getServerSidePropsForSingleFormView = async function getServerSidePropsForSingleFormView( - context: AppGetServerSidePropsContext, - prisma: AppPrisma, - user: AppUser, - ssrInit: AppSsrInit -) { - const ssr = await ssrInit(context); - - if (!user) { - return { - redirect: { - permanent: false, - destination: "/auth/login", - }, - }; - } - const { params } = context; - if (!params) { - return { - notFound: true, - }; - } - const formId = params.appPages[0]; - if (!formId || params.appPages.length > 1) { - return { - notFound: true, - }; - } - - const isFormCreateEditAllowed = (await import("../lib/isFormCreateEditAllowed")).isFormCreateEditAllowed; - if (!(await isFormCreateEditAllowed({ userId: user.id, formId, targetTeamId: null }))) { - return { - notFound: true, - }; - } - - const form = await prisma.app_RoutingForms_Form.findUnique({ - where: { - id: formId, - }, - include: { - team: { - select: { - name: true, - slug: true, - }, - }, - _count: { - select: { - responses: true, - }, - }, - }, - }); - if (!form) { - return { - notFound: true, - }; - } - - return { - props: { - trpcState: ssr.dehydrate(), - form: await getSerializableForm({ form }), - }, - }; -}; +export { getServerSidePropsForSingleFormView }; diff --git a/packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts b/packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts new file mode 100644 index 0000000000..0998c17366 --- /dev/null +++ b/packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts @@ -0,0 +1,76 @@ +import type { + AppGetServerSidePropsContext, + AppPrisma, + AppSsrInit, + AppUser, +} from "@calcom/types/AppGetServerSideProps"; + +import { getSerializableForm } from "../lib/getSerializableForm"; + +export const getServerSidePropsForSingleFormView = async function getServerSidePropsForSingleFormView( + context: AppGetServerSidePropsContext, + prisma: AppPrisma, + user: AppUser, + ssrInit: AppSsrInit +) { + const ssr = await ssrInit(context); + + if (!user) { + return { + redirect: { + permanent: false, + destination: "/auth/login", + }, + }; + } + const { params } = context; + if (!params) { + return { + notFound: true, + }; + } + const formId = params.appPages[0]; + if (!formId || params.appPages.length > 1) { + return { + notFound: true, + }; + } + + const isFormCreateEditAllowed = (await import("../lib/isFormCreateEditAllowed")).isFormCreateEditAllowed; + if (!(await isFormCreateEditAllowed({ userId: user.id, formId, targetTeamId: null }))) { + return { + notFound: true, + }; + } + + const form = await prisma.app_RoutingForms_Form.findUnique({ + where: { + id: formId, + }, + include: { + team: { + select: { + name: true, + slug: true, + }, + }, + _count: { + select: { + responses: true, + }, + }, + }, + }); + if (!form) { + return { + notFound: true, + }; + } + + return { + props: { + trpcState: await ssr.dehydrate(), + form: await getSerializableForm({ form }), + }, + }; +}; diff --git a/packages/app-store/routing-forms/lib/processRoute.tsx b/packages/app-store/routing-forms/lib/processRoute.tsx index d1c20a12d4..662bc81480 100644 --- a/packages/app-store/routing-forms/lib/processRoute.tsx +++ b/packages/app-store/routing-forms/lib/processRoute.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { App_RoutingForms_Form } from "@prisma/client"; import { Utils as QbUtils } from "react-awesome-query-builder"; import type { z } from "zod"; 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 151352ef3d..775861594a 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,18 @@ //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 LayoutHandler from "./layout-handler/[...appPages]"; import * as Reporting from "./reporting/[...appPages]"; import * as RouteBuilder from "./route-builder/[...appPages]"; import * as Router from "./router/[...appPages]"; +import { getServerSideProps as getServerSidePropsRouter } from "./router/getServerSideProps"; import * as RoutingLink from "./routing-link/[...appPages]"; +import { getServerSideProps as getServerSidePropsRoutingLink } from "./routing-link/getServerSideProps"; const routingConfig = { "form-edit": formEdit, @@ -17,4 +24,13 @@ const routingConfig = { layoutHandler: LayoutHandler, }; +export const serverSidePropsConfig: Record = { + forms: getServerSidePropsForms, + "form-edit": getServerSidePropsSingleForm, + "route-builder": getServerSidePropsSingleForm, + "routing-link": getServerSidePropsRoutingLink, + router: getServerSidePropsRouter, + reporting: 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 901bc21be0..2c9abde29e 100644 --- a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; diff --git a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx index dd32b58d84..1826b06b75 100644 --- a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + // TODO: i18n import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect } from "react"; @@ -16,12 +18,6 @@ import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { trpc } from "@calcom/trpc/react"; -import type { - AppGetServerSidePropsContext, - AppPrisma, - AppSsrInit, - AppUser, -} from "@calcom/types/AppGetServerSideProps"; import { Badge, Button, @@ -61,6 +57,7 @@ import { } from "../../components/FormActions"; import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; import { isFallbackRoute } from "../../lib/isFallbackRoute"; +import { getServerSideProps } from "./getServerSideProps"; function NewFormButton() { const { t } = useLocale(); @@ -372,32 +369,4 @@ RoutingForms.getLayout = (page: React.ReactElement) => { ); }; -export const getServerSideProps = async function getServerSideProps( - context: AppGetServerSidePropsContext, - prisma: AppPrisma, - user: AppUser, - ssrInit: AppSsrInit -) { - if (!user) { - return { - redirect: { - permanent: false, - destination: "/auth/login", - }, - }; - } - const ssr = await ssrInit(context); - - const filters = getTeamsFiltersFromQuery(context.query); - - await ssr.viewer.appRoutingForms.forms.prefetch({ - filters, - }); - // Prefetch this so that New Button is immediately available - await ssr.viewer.teamsAndUserProfilesQuery.prefetch(); - return { - props: { - trpcState: ssr.dehydrate(), - }, - }; -}; +export { getServerSideProps }; diff --git a/packages/app-store/routing-forms/pages/forms/getServerSideProps.ts b/packages/app-store/routing-forms/pages/forms/getServerSideProps.ts new file mode 100644 index 0000000000..345c0b3e5f --- /dev/null +++ b/packages/app-store/routing-forms/pages/forms/getServerSideProps.ts @@ -0,0 +1,37 @@ +import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; +import type { + AppGetServerSidePropsContext, + AppPrisma, + AppSsrInit, + AppUser, +} from "@calcom/types/AppGetServerSideProps"; + +export const getServerSideProps = async function getServerSideProps( + context: AppGetServerSidePropsContext, + prisma: AppPrisma, + user: AppUser, + ssrInit: AppSsrInit +) { + if (!user) { + return { + redirect: { + permanent: false, + destination: "/auth/login", + }, + }; + } + const ssr = await ssrInit(context); + + const filters = getTeamsFiltersFromQuery(context.query); + + await ssr.viewer.appRoutingForms.forms.prefetch({ + filters, + }); + // Prefetch this so that New Button is immediately available + await ssr.viewer.teamsAndUserProfilesQuery.prefetch(); + return { + props: { + trpcState: await ssr.dehydrate(), + }, + }; +}; diff --git a/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx b/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx index 778a4fd825..5d1058cb28 100644 --- a/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx @@ -1,5 +1,6 @@ +"use client"; + import type { GetServerSidePropsContext } from "next"; -import type { AppProps as NextAppProps } from "next/app"; import React from "react"; import { FormProvider, useForm } from "react-hook-form"; @@ -10,22 +11,23 @@ import type { AppProps } from "@lib/app-providers"; import RoutingFormsRoutingConfig from "../app-routing.config"; +const DEFAULT_ROUTE = "forms"; + type GetServerSidePropsRestArgs = [AppPrisma, AppUser, AppSsrInit]; type Component = { default: React.ComponentType & Pick; getServerSideProps?: (context: GetServerSidePropsContext, ...rest: GetServerSidePropsRestArgs) => void; }; -const getComponent = (route: string | NextAppProps["router"]): Component => { - const defaultRoute = "forms"; - const routeKey = - typeof route === "string" ? route || defaultRoute : route?.query?.pages?.[0] || defaultRoute; - return (RoutingFormsRoutingConfig as unknown as Record)[routeKey]; +const getComponent = (route: string): Component => { + return (RoutingFormsRoutingConfig as unknown as Record)[route]; }; export default function LayoutHandler(props: { [key: string]: unknown }) { const params = useParamsWithFallback(); const methods = useForm(); - const pageKey = params?.pages?.[0] || "forms"; + const pageKey = Array.isArray(params.pages) + ? params.pages[0] + : params.pages?.split("/")[0] ?? DEFAULT_ROUTE; const PageComponent = getComponent(pageKey).default; return ( @@ -34,10 +36,16 @@ export default function LayoutHandler(props: { [key: string]: unknown }) { ); } -LayoutHandler.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => { - const component = getComponent(router).default; +LayoutHandler.getLayout = (page: React.ReactElement) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const params = useParamsWithFallback(); + const pageKey = Array.isArray(params.pages) + ? params.pages[0] + : params.pages?.split("/")[0] ?? DEFAULT_ROUTE; + + const component = getComponent(pageKey).default; if (component && "getLayout" in component) { - return component.getLayout?.(page, router); + return component.getLayout?.(page); } else { return page; } diff --git a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx b/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx index b92ab481d6..7a55917caa 100644 --- a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useCallback, useRef, useState } from "react"; import type { BuilderProps, 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 fbc2a0cc91..0337f53133 100644 --- a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + import { useAutoAnimate } from "@formkit/auto-animate/react"; import Link from "next/link"; import React, { useCallback, useState } from "react"; diff --git a/packages/app-store/routing-forms/pages/router/[...appPages].tsx b/packages/app-store/routing-forms/pages/router/[...appPages].tsx index c97c10a935..c361190a01 100644 --- a/packages/app-store/routing-forms/pages/router/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/router/[...appPages].tsx @@ -1,21 +1,11 @@ -import Head from "next/head"; -import { stringify } from "querystring"; -import z from "zod"; +"use client"; + +import Head from "next/head"; -import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import logger from "@calcom/lib/logger"; -import { TRPCError } from "@calcom/trpc"; -import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import getFieldIdentifier from "../../lib/getFieldIdentifier"; -import { getSerializableForm } from "../../lib/getSerializableForm"; -import { processRoute } from "../../lib/processRoute"; -import transformResponse from "../../lib/transformResponse"; -import type { Response } from "../../types/types"; +import { getServerSideProps } from "./getServerSideProps"; -const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] }); export default function Router({ form, message }: inferSSRProps) { return ( <> @@ -33,111 +23,4 @@ export default function Router({ form, message }: inferSSRProps { - const fieldResponse = fieldsResponses[getFieldIdentifier(field)] || ""; - - response[field.id] = { - label: field.label, - value: transformResponse({ field, value: fieldResponse }), - }; - }); - - const decidedAction = processRoute({ form: serializableForm, response }); - - if (!decidedAction) { - throw new Error("No matching route could be found"); - } - - const { createContext } = await import("@calcom/trpc/server/createContext"); - const ctx = await createContext(context); - - const { default: trpcRouter } = await import("@calcom/app-store/routing-forms/trpc/_router"); - const caller = trpcRouter.createCaller(ctx); - const { v4: uuidv4 } = await import("uuid"); - try { - await caller.public.response({ - formId: form.id, - formFillerId: uuidv4(), - response: response, - }); - } catch (e) { - if (e instanceof TRPCError) { - return { - props: { - form: serializableForm, - message: e.message, - }, - }; - } - } - - //TODO: Maybe take action after successful mutation - if (decidedAction.type === "customPageMessage") { - return { - props: { - form: serializableForm, - message: decidedAction.value, - }, - }; - } else if (decidedAction.type === "eventTypeRedirectUrl") { - return { - redirect: { - destination: `/${decidedAction.value}?${stringify(context.query)}`, - permanent: false, - }, - }; - } else if (decidedAction.type === "externalRedirectUrl") { - return { - redirect: { - destination: `${decidedAction.value}?${stringify(context.query)}`, - permanent: false, - }, - }; - } - - return { - props: { - form: serializableForm, - }, - }; -}; +export { getServerSideProps }; diff --git a/packages/app-store/routing-forms/pages/router/getServerSideProps.ts b/packages/app-store/routing-forms/pages/router/getServerSideProps.ts new file mode 100644 index 0000000000..06ed631a01 --- /dev/null +++ b/packages/app-store/routing-forms/pages/router/getServerSideProps.ts @@ -0,0 +1,125 @@ +import { stringify } from "querystring"; +import z from "zod"; + +import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import logger from "@calcom/lib/logger"; +import { TRPCError } from "@calcom/trpc/server"; +import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps"; + +import getFieldIdentifier from "../../lib/getFieldIdentifier"; +import { getSerializableForm } from "../../lib/getSerializableForm"; +import { processRoute } from "../../lib/processRoute"; +import transformResponse from "../../lib/transformResponse"; +import type { Response } from "../../types/types"; + +const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] }); + +const querySchema = z + .object({ + form: z.string(), + slug: z.string(), + pages: z.array(z.string()), + }) + .catchall(z.string().or(z.array(z.string()))); + +export const getServerSideProps = async function getServerSideProps( + context: AppGetServerSidePropsContext, + prisma: AppPrisma +) { + const queryParsed = querySchema.safeParse(context.query); + if (!queryParsed.success) { + log.warn("Error parsing query", queryParsed.error); + return { + notFound: true, + }; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data; + const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req); + + const form = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: formId, + user: { + organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null, + }, + }, + }); + + if (!form) { + return { + notFound: true, + }; + } + const serializableForm = await getSerializableForm({ form }); + + const response: Response = {}; + serializableForm.fields?.forEach((field) => { + const fieldResponse = fieldsResponses[getFieldIdentifier(field)] || ""; + + response[field.id] = { + label: field.label, + value: transformResponse({ field, value: fieldResponse }), + }; + }); + + const decidedAction = processRoute({ form: serializableForm, response }); + + if (!decidedAction) { + throw new Error("No matching route could be found"); + } + + const { createContext } = await import("@calcom/trpc/server/createContext"); + const ctx = await createContext(context); + + const { default: trpcRouter } = await import("@calcom/app-store/routing-forms/trpc/_router"); + const caller = trpcRouter.createCaller(ctx); + const { v4: uuidv4 } = await import("uuid"); + try { + await caller.public.response({ + formId: form.id, + formFillerId: uuidv4(), + response: response, + }); + } catch (e) { + if (e instanceof TRPCError) { + return { + props: { + form: serializableForm, + message: e.message, + }, + }; + } + } + + //TODO: Maybe take action after successful mutation + if (decidedAction.type === "customPageMessage") { + return { + props: { + form: serializableForm, + message: decidedAction.value, + }, + }; + } else if (decidedAction.type === "eventTypeRedirectUrl") { + return { + redirect: { + destination: `/${decidedAction.value}?${stringify(context.query)}`, + permanent: false, + }, + }; + } else if (decidedAction.type === "externalRedirectUrl") { + return { + redirect: { + destination: `${decidedAction.value}?${stringify(context.query)}`, + permanent: false, + }, + }; + } + + return { + props: { + form: serializableForm, + }, + }; +}; 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 ae2aab94ec..c3e39dc596 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx @@ -1,3 +1,5 @@ +"use client"; + import Head from "next/head"; import { useRouter } from "next/navigation"; import type { FormEvent } from "react"; @@ -6,23 +8,21 @@ import { Toaster } from "react-hot-toast"; import { v4 as uuidv4 } from "uuid"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import classNames from "@calcom/lib/classNames"; import useGetBrandingColours from "@calcom/lib/getBrandColours"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { trpc } from "@calcom/trpc/react"; -import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Button, showToast, useCalcomTheme } from "@calcom/ui"; import FormInputFields from "../../components/FormInputFields"; import getFieldIdentifier from "../../lib/getFieldIdentifier"; -import { getSerializableForm } from "../../lib/getSerializableForm"; import { processRoute } from "../../lib/processRoute"; import transformResponse from "../../lib/transformResponse"; import type { Response, Route } from "../../types/types"; +import { getServerSideProps } from "./getServerSideProps"; type Props = inferSSRProps; const useBrandColors = ({ @@ -233,68 +233,7 @@ export default function RoutingLink(props: inferSSRProps 2) { - return { - notFound: true, - }; - } - const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req); - - const isEmbed = params.appPages[1] === "embed"; - - const form = await prisma.app_RoutingForms_Form.findFirst({ - where: { - id: formId, - user: { - organization: isValidOrgDomain - ? { - slug: currentOrgDomain, - } - : null, - }, - }, - include: { - user: { - select: { - username: true, - theme: true, - brandColor: true, - darkBrandColor: true, - }, - }, - }, - }); - - if (!form || form.disabled) { - return { - notFound: true, - }; - } - - return { - props: { - isEmbed, - themeBasis: form.user.username, - profile: { - theme: form.user.theme, - brandColor: form.user.brandColor, - darkBrandColor: form.user.darkBrandColor, - }, - form: await getSerializableForm({ form }), - }, - }; -}; +export { getServerSideProps }; const usePrefilledResponse = (form: Props["form"]) => { const searchParams = useCompatSearchParams(); diff --git a/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts b/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts new file mode 100644 index 0000000000..31b8e7cc4c --- /dev/null +++ b/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts @@ -0,0 +1,67 @@ +import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps"; + +import { getSerializableForm } from "../../lib/getSerializableForm"; + +export const getServerSideProps = async function getServerSideProps( + context: AppGetServerSidePropsContext, + prisma: AppPrisma +) { + const { params } = context; + if (!params) { + return { + notFound: true, + }; + } + const formId = params.appPages[0]; + if (!formId || params.appPages.length > 2) { + return { + notFound: true, + }; + } + const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req); + + const isEmbed = params.appPages[1] === "embed"; + + const form = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: formId, + user: { + organization: isValidOrgDomain + ? { + slug: currentOrgDomain, + } + : null, + }, + }, + include: { + user: { + select: { + username: true, + theme: true, + brandColor: true, + darkBrandColor: true, + }, + }, + }, + }); + + if (!form || form.disabled) { + return { + notFound: true, + }; + } + + return { + props: { + isEmbed, + themeBasis: form.user.username, + profile: { + theme: form.user.theme, + brandColor: form.user.brandColor, + darkBrandColor: form.user.darkBrandColor, + }, + form: await getSerializableForm({ form }), + }, + }; +}; diff --git a/packages/app-store/typeform/pages/app-routing.config.tsx b/packages/app-store/typeform/pages/app-routing.config.tsx index 11a6afa571..c0f855f1a6 100644 --- a/packages/app-store/typeform/pages/app-routing.config.tsx +++ b/packages/app-store/typeform/pages/app-routing.config.tsx @@ -1,3 +1,5 @@ +"use client"; + //TODO: Generate this file automatically so that like in Next.js file based routing can work automatically import * as HowToUse from "./how-to-use/[...appPages]"; diff --git a/packages/emails/src/templates/AdminOrganizationNotificationEmail.tsx b/packages/emails/src/templates/AdminOrganizationNotificationEmail.tsx index 1449a2ed98..b6e9198e56 100644 --- a/packages/emails/src/templates/AdminOrganizationNotificationEmail.tsx +++ b/packages/emails/src/templates/AdminOrganizationNotificationEmail.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Trans, type TFunction } from "next-i18next"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; diff --git a/packages/emails/src/templates/BrokenIntegrationEmail.tsx b/packages/emails/src/templates/BrokenIntegrationEmail.tsx index 4271d68c91..d7ebe70026 100644 --- a/packages/emails/src/templates/BrokenIntegrationEmail.tsx +++ b/packages/emails/src/templates/BrokenIntegrationEmail.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { TFunction } from "next-i18next"; import { Trans } from "react-i18next"; diff --git a/packages/emails/src/templates/SlugReplacementEmail.tsx b/packages/emails/src/templates/SlugReplacementEmail.tsx index 8a889456ef..c40ebdb558 100644 --- a/packages/emails/src/templates/SlugReplacementEmail.tsx +++ b/packages/emails/src/templates/SlugReplacementEmail.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { TFunction } from "next-i18next"; import { Trans } from "next-i18next"; diff --git a/packages/features/filters/lib/getTeamsFiltersFromQuery.ts b/packages/features/filters/lib/getTeamsFiltersFromQuery.ts index f0c1aad074..b095fb922e 100644 --- a/packages/features/filters/lib/getTeamsFiltersFromQuery.ts +++ b/packages/features/filters/lib/getTeamsFiltersFromQuery.ts @@ -1,5 +1,3 @@ -"use client"; - import type { ParsedUrlQuery } from "querystring"; import { z } from "zod";