chore: [app-router-migration 16]: migrate routing forms page (#13015)

Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
DmytroHryshyn
2024-01-19 11:57:08 -07:00
committed by GitHub
co-authored by Benny Joo
parent dd7673b1f9
commit bdd1d3a99d
29 changed files with 666 additions and 334 deletions
@@ -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<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
};
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<string, AppPageType>;
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 (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<LegacyPage {...props} />
</PageWrapper>
);
}
@@ -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;
@@ -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;
+1 -2
View File
@@ -89,8 +89,7 @@ function PageWrapper(props: AppProps) {
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
),
router
)
)}
</AppProviders>
);
+1 -1
View File
@@ -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;
};
+11
View File
@@ -0,0 +1,11 @@
// returns query object same as ctx.query but for app dir
export const getQuery = (url: string, params: Record<string, string | string[]>) => {
if (!url.length) {
return params;
}
const { searchParams } = new URL(url);
const searchParamsObj = Object.fromEntries(searchParams.entries());
return { ...searchParamsObj, ...params };
};
+1
View File
@@ -116,6 +116,7 @@ export const config = {
* Paths required by routingForms.handle
*/
"/apps/routing_forms/:path*",
"/event-types",
"/future/event-types/",
"/settings/admin/:path*",
+10 -5
View File
@@ -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;
@@ -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 (
<WizardLayout
currentStep={5}
maxSteps={5}
@@ -32,8 +34,8 @@ AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextAppProps["rou
}}>
{page}
</WizardLayout>
</>
);
);
};
AddNewTeamsPage.PageWrapper = PageWrapper;
@@ -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"]) => (
<WizardLayout
currentStep={4}
maxSteps={5}
isOptionalCallback={() => {
router.push(`/settings/organizations/${router.query.id}/add-teams`);
}}>
{page}
</WizardLayout>
);
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 (
<WizardLayout
currentStep={4}
maxSteps={5}
isOptionalCallback={() => {
router.push(`/settings/organizations/${query.get("id")}/add-teams`);
}}>
{page}
</WizardLayout>
);
};
export const buildWrappedOnboardTeamMembersPage = (
id: string | string[] | undefined,
@@ -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<App_RoutingForms_Form>;
@@ -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 };
@@ -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 }),
},
};
};
@@ -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";
@@ -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<string, AppGetServerSideProps> = {
forms: getServerSidePropsForms,
"form-edit": getServerSidePropsSingleForm,
"route-builder": getServerSidePropsSingleForm,
"routing-link": getServerSidePropsRoutingLink,
router: getServerSidePropsRouter,
reporting: getServerSidePropsSingleForm,
};
export default routingConfig;
@@ -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";
@@ -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 };
@@ -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(),
},
};
};
@@ -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<AppProps["Component"], "getLayout">;
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<string, Component>)[routeKey];
const getComponent = (route: string): Component => {
return (RoutingFormsRoutingConfig as unknown as Record<string, Component>)[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 (
<FormProvider {...methods}>
@@ -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;
}
@@ -1,3 +1,5 @@
"use client";
import React, { useCallback, useRef, useState } from "react";
import type {
BuilderProps,
@@ -1,3 +1,5 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import React, { useCallback, useState } from "react";
@@ -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<typeof getServerSideProps>) {
return (
<>
@@ -33,111 +23,4 @@ export default function Router({ form, message }: inferSSRProps<typeof getServer
);
}
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,
},
};
};
export { getServerSideProps };
@@ -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,
},
};
};
@@ -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<typeof getServerSideProps>;
const useBrandColors = ({
@@ -233,68 +233,7 @@ export default function RoutingLink(props: inferSSRProps<typeof getServerSidePro
RoutingLink.isBookingPage = true;
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 }),
},
};
};
export { getServerSideProps };
const usePrefilledResponse = (form: Props["form"]) => {
const searchParams = useCompatSearchParams();
@@ -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 }),
},
};
};
@@ -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]";
@@ -1,3 +1,5 @@
"use client";
import { Trans, type TFunction } from "next-i18next";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
@@ -1,3 +1,5 @@
"use client";
import type { TFunction } from "next-i18next";
import { Trans } from "react-i18next";
@@ -1,3 +1,5 @@
"use client";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
@@ -1,5 +1,3 @@
"use client";
import type { ParsedUrlQuery } from "querystring";
import { z } from "zod";