diff --git a/PR_TODO.md b/PR_TODO.md new file mode 100644 index 00000000000000..1cadb3d2331ca9 --- /dev/null +++ b/PR_TODO.md @@ -0,0 +1 @@ +- Webhooks iterate over apps to identify routing webhooks - It needs to be fixed diff --git a/apps/web/components/dialog/RerouteDialog.tsx b/apps/web/components/dialog/RerouteDialog.tsx index ca72e40dc83ad2..3dacc107bb3c1c 100644 --- a/apps/web/components/dialog/RerouteDialog.tsx +++ b/apps/web/components/dialog/RerouteDialog.tsx @@ -5,17 +5,17 @@ import { useState } from "react"; import { useEffect, useCallback } from "react"; import type { z } from "zod"; -import FormInputFields, { - FormInputFieldsSkeleton, -} from "@calcom/app-store/routing-forms/components/FormInputFields"; -import { getAbsoluteEventTypeRedirectUrl } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl"; -import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute"; -import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables"; -import { getUrlSearchParamsToForwardForReroute } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward"; -import type { FormResponse, LocalRoute } from "@calcom/app-store/routing-forms/types/types"; -import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; import dayjs from "@calcom/dayjs"; import { createBooking } from "@calcom/features/bookings/lib/create-booking"; +import FormInputFields, { + FormInputFieldsSkeleton, +} from "@calcom/features/routing-forms/components/FormInputFields"; +import { getAbsoluteEventTypeRedirectUrl } from "@calcom/features/routing-forms/getEventTypeRedirectUrl"; +import { getUrlSearchParamsToForwardForReroute } from "@calcom/features/routing-forms/lib/getUrlSearchParamsToForward"; +import { findMatchingRoute } from "@calcom/features/routing-forms/lib/processRoute"; +import { substituteVariables } from "@calcom/features/routing-forms/lib/substituteVariables"; +import type { FormResponse, LocalRoute } from "@calcom/features/routing-forms/types/types"; +import { RouteActionType } from "@calcom/features/routing-forms/zod"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { EventType, User, Team, Attendee, Booking as PrismaBooking } from "@calcom/prisma/client"; @@ -35,7 +35,7 @@ const enum ReroutingStatusEnum { REROUTING_FAILED = "failed", } -type ResponseWithForm = RouterOutputs["viewer"]["appRoutingForms"]["getResponseWithFormFields"]; +type ResponseWithForm = RouterOutputs["viewer"]["routingForms"]["getResponseWithFormFields"]; type BookingToReroute = Pick & { routedFromRoutingFormReponse: { diff --git a/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx b/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx index e5c68d786e9a73..f2f26eba32d6a6 100644 --- a/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx +++ b/apps/web/components/dialog/__tests__/RerouteDialog.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { vi } from "vitest"; -import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; +import { RouteActionType } from "@calcom/features/routing-forms/zod"; import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; import { RerouteDialog } from "../RerouteDialog"; @@ -20,7 +20,7 @@ vi.mock("next/navigation", async (importOriginal) => { }; }); -vi.mock("@calcom/app-store/routing-forms/lib/processRoute", () => ({ +vi.mock("@calcom/features/routing-forms/lib/processRoute", () => ({ findMatchingRoute: vi.fn(({ form, response }) => { return form.routes.find((route: any) => route.__testMatching); }), @@ -36,7 +36,7 @@ const mockOpen = vi.fn((_url: string) => { vi.stubGlobal("open", mockOpen); -vi.mock("@calcom/app-store/routing-forms/components/FormInputFields", () => ({ +vi.mock("@calcom/features/routing-forms/components/FormInputFields", () => ({ default: vi.fn(({ response, form, setResponse, disabledFields }) => { return (
diff --git a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts index e5f67ae1c127a5..a858560e94a839 100644 --- a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts +++ b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts @@ -4,14 +4,14 @@ import { v4 } from "uuid"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip"; -import bookingFormHandlers from "@calcom/app-store/routing-forms/appBookingFormHandler"; -import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants"; -import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; +import bookingFormHandlers from "@calcom/features/routing-forms/appBookingFormHandler"; +import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/features/routing-forms/lib/constants"; +import { RouteActionType } from "@calcom/features/routing-forms/zod"; import { SchedulingType } from "@calcom/prisma/enums"; import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "../getTeamMemberEmailFromCrm"; -vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({ +vi.mock("@calcom/features/routing-forms/appBookingFormHandler", () => ({ default: { salesforce: vi.fn(), }, diff --git a/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts b/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts index 5cd62aade0c127..28e8883c3045ed 100644 --- a/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts +++ b/apps/web/lib/apps/[slug]/[...pages]/getServerSideProps.ts @@ -1,59 +1,6 @@ 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"; -import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"; - -import type { AppProps } from "@lib/app-providers"; - -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"]; -}; - -type NotFound = { - notFound: true; -}; - -// TODO: It is a candidate for apps.*.generated.* -const AppsRouting = { - "routing-forms": RoutingFormsRoutingConfig, - typeform: TypeformRoutingConfig, -}; - -function getRoute(appName: string, pages: string[]) { - const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record; - - if (!routingConfig) { - return { - notFound: true, - } as NotFound; - } - const mainPage = pages[0]; - const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType); - if (!appPage) { - return { - notFound: true, - } as NotFound; - } - return { notFound: false, Component: appPage.default, ...appPage } as Found; -} - const paramsSchema = z.object({ slug: z.string(), pages: z.array(z.string()), @@ -62,7 +9,7 @@ const paramsSchema = z.object({ export async function getServerSideProps( context: GetServerSidePropsContext ): Promise> { - const { params, req } = context; + const { params } = context; if (!params) { return { notFound: true, @@ -78,58 +25,12 @@ export async function getServerSideProps( const appName = parsedParams.data.slug; const pages = parsedParams.data.pages; - const route = getRoute(appName, pages); - - if (route.notFound) { - return { notFound: true }; - } - - 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 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 || `/apps/${appName}`, - ...result.props, - }, - }; - } else { - return { - props: { - appName, - }, - }; - } + // Redirect to the actual page using Next.js routing + return { + redirect: { + destination: `/${appName}/${pages.join("/")}`, + permanent: false, + }, + }; } diff --git a/apps/web/lib/getTeamMemberEmailFromCrm.ts b/apps/web/lib/getTeamMemberEmailFromCrm.ts index 0abc54e904c4a7..9894a8c0e89cc6 100644 --- a/apps/web/lib/getTeamMemberEmailFromCrm.ts +++ b/apps/web/lib/getTeamMemberEmailFromCrm.ts @@ -1,10 +1,10 @@ import type { ParsedUrlQuery } from "querystring"; import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip"; -import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants"; -import { enabledAppSlugs } from "@calcom/app-store/routing-forms/lib/enabledApps"; -import type { AttributeRoutingConfig, LocalRoute } from "@calcom/app-store/routing-forms/types/types"; -import { zodRoutes as routesSchema } from "@calcom/app-store/routing-forms/zod"; +import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/features/routing-forms/lib/constants"; +import { enabledAppSlugs } from "@calcom/features/routing-forms/lib/enabledApps"; +import type { AttributeRoutingConfig, LocalRoute } from "@calcom/features/routing-forms/types/types"; +import { zodRoutes as routesSchema } from "@calcom/features/routing-forms/zod"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import prisma from "@calcom/prisma"; @@ -151,7 +151,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({ // If the skipContactOwner is enabled then don't return an team member email if (attributeRoutingConfig.skipContactOwner) return { ...nullReturnValue, skipContactOwner: true }; - const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler")) + const appBookingFormHandler = (await import("@calcom/features/routing-forms/appBookingFormHandler")) .default; const appHandler = appBookingFormHandler[crmAppSlug]; if (!appHandler) return nullReturnValue; diff --git a/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx b/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx index 1a1ca5d06fba99..464cd901db244c 100644 --- a/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx +++ b/apps/web/modules/apps/[slug]/[...pages]/pages-view.tsx @@ -1,6 +1,5 @@ "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"; @@ -29,7 +28,6 @@ type NotFound = { // TODO: It is a candidate for apps.*.generated.* const AppsRouting = { - "routing-forms": RoutingFormsRoutingConfig, typeform: TypeformRoutingConfig, }; diff --git a/apps/web/modules/insights/insights-virtual-queues-view.tsx b/apps/web/modules/insights/insights-virtual-queues-view.tsx index a07b1984061e70..dde6f82b65eb02 100644 --- a/apps/web/modules/insights/insights-virtual-queues-view.tsx +++ b/apps/web/modules/insights/insights-virtual-queues-view.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; -import { TestForm } from "@calcom/app-store/routing-forms/components/SingleForm"; -import type { RoutingForm } from "@calcom/app-store/routing-forms/types/types"; +import { TestForm } from "@calcom/features/routing-forms/components/SingleForm"; +import type { RoutingForm } from "@calcom/features/routing-forms/types/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { Label, Select } from "@calcom/ui"; diff --git a/apps/web/pages/api/trpc/appRoutingForms/[trpc].ts b/apps/web/pages/api/trpc/appRoutingForms/[trpc].ts index 63037301d8c3f3..b0e7f72065df2f 100644 --- a/apps/web/pages/api/trpc/appRoutingForms/[trpc].ts +++ b/apps/web/pages/api/trpc/appRoutingForms/[trpc].ts @@ -1,4 +1,4 @@ -import appRoutingForms from "@calcom/app-store/routing-forms/trpc-router"; +import appRoutingForms from "@calcom/features/routing-forms/trpc-router"; import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler"; export default createNextApiHandler(appRoutingForms); diff --git a/apps/web/pages/apps/[slug]/[...pages].tsx b/apps/web/pages/apps/[slug]/[...pages].tsx index 6de4cdfd96f81a..816e3f107e4310 100644 --- a/apps/web/pages/apps/[slug]/[...pages].tsx +++ b/apps/web/pages/apps/[slug]/[...pages].tsx @@ -1,15 +1,11 @@ -import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps"; +import type { GetServerSideProps } from "next"; -import PageWrapper from "@components/PageWrapper"; +export const getServerSideProps: GetServerSideProps = async (context) => { + return { + notFound: true, // This will let Next.js handle the routing through the pages directory + }; +}; -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; +export default function AppPage() { + return null; +} diff --git a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx b/apps/web/pages/routing/form-edit/[id].tsx similarity index 96% rename from packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx rename to apps/web/pages/routing/form-edit/[id].tsx index 2aa2e63f43188a..194449d0138cc2 100644 --- a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx +++ b/apps/web/pages/routing/form-edit/[id].tsx @@ -6,9 +6,14 @@ import type { UseFormReturn } from "react-hook-form"; import { Controller, useFieldArray, useWatch } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; -import Shell from "@calcom/features/shell/Shell"; +import type { RoutingFormWithResponseCount } from "@calcom/features/routing-forms/components/SingleForm"; +import SingleForm from "@calcom/features/routing-forms/components/SingleForm"; +import getLayout from "@calcom/features/routing-forms/getLayout"; +import { getServerSideProps } from "@calcom/features/routing-forms/getServerSidePropsSingleForm"; +import { FieldTypes } from "@calcom/features/routing-forms/lib/FieldTypes"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { BooleanToggleGroupField, Button, @@ -21,15 +26,10 @@ import { TextField, } from "@calcom/ui"; -import type { inferSSRProps } from "@lib/types/inferSSRProps"; - -import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; -import SingleForm, { - getServerSidePropsForSingleFormView as getServerSideProps, -} from "../../components/SingleForm"; -import { FieldTypes } from "../../lib/FieldTypes"; +import PageWrapper from "@components/PageWrapper"; export { getServerSideProps }; + type SelectOption = { label: string; id: string | null }; type HookForm = UseFormReturn; @@ -480,10 +480,6 @@ export default function FormEditPage({ ); } -FormEditPage.getLayout = (page: React.ReactElement) => { - return ( - - {page} - - ); -}; +FormEditPage.getLayout = getLayout; + +FormEditPage.PageWrapper = PageWrapper; diff --git a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx b/apps/web/pages/routing/forms/index.tsx similarity index 96% rename from packages/app-store/routing-forms/pages/forms/[...appPages].tsx rename to apps/web/pages/routing/forms/index.tsx index b70747ae9c8fc0..121e315577169b 100644 --- a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx +++ b/apps/web/pages/routing/forms/index.tsx @@ -9,6 +9,15 @@ import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/Skeleto import { FilterResults } from "@calcom/features/filters/components/FilterResults"; import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter"; import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; +import { + FormAction, + FormActionsDropdown, + FormActionsProvider, + useOpenModal, +} from "@calcom/features/routing-forms/components/FormActions"; +import type { RoutingFormWithResponseCount } from "@calcom/features/routing-forms/components/SingleForm"; +import { getServerSideProps } from "@calcom/features/routing-forms/getServerSidePropsForms"; +import { isFallbackRoute } from "@calcom/features/routing-forms/lib/isFallbackRoute"; import Shell, { ShellMain } from "@calcom/features/shell/Shell"; import { UpgradeTip } from "@calcom/features/tips"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -32,16 +41,6 @@ import { import type { inferSSRProps } from "@lib/types/inferSSRProps"; -import { - FormAction, - FormActionsDropdown, - FormActionsProvider, - useOpenModal, -} from "../../components/FormActions"; -import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; -import { isFallbackRoute } from "../../lib/isFallbackRoute"; -import { getServerSideProps } from "./getServerSideProps"; - function NewFormButton() { const { t } = useLocale(); const openModal = useOpenModal(); @@ -56,11 +55,7 @@ function NewFormButton() { ); } -export default function RoutingForms({ - appUrl, -}: inferSSRProps & { - appUrl: string; -}) { +export default function RoutingForms({ appUrl }: inferSSRProps) { const { t } = useLocale(); const { hasPaidPlan } = useHasPaidPlan(); const routerQuery = useRouterQuery(); @@ -221,7 +216,7 @@ export default function RoutingForms({ moveRoutingForm(index, 1)} arrowDirection="down" /> )} ); } - -IncompleteBookingPage.getLayout = (page: React.ReactElement) => { - return ( - - {page} - - ); -}; +IncompleteBookingPage.getLayout = getLayout; +IncompleteBookingPage.PageWrapper = PageWrapper; export { getServerSideProps }; diff --git a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx b/apps/web/pages/routing/reporting/[id].tsx similarity index 93% rename from packages/app-store/routing-forms/pages/reporting/[...appPages].tsx rename to apps/web/pages/routing/reporting/[id].tsx index 855cdeef798a5e..c3a82bd16b47f5 100644 --- a/packages/app-store/routing-forms/pages/reporting/[...appPages].tsx +++ b/apps/web/pages/routing/reporting/[id].tsx @@ -10,7 +10,18 @@ import type { } from "react-awesome-query-builder"; import { Builder, Query, Utils as QbUtils } from "react-awesome-query-builder"; -import Shell from "@calcom/features/shell/Shell"; +import SingleForm from "@calcom/features/routing-forms/components/SingleForm"; +import { + withRaqbSettingsAndWidgets, + ConfigFor, +} from "@calcom/features/routing-forms/components/react-awesome-query-builder/config/uiConfig"; +import getLayout from "@calcom/features/routing-forms/getLayout"; +import { getServerSideProps } from "@calcom/features/routing-forms/getServerSidePropsSingleForm"; +import type { JsonLogicQuery } from "@calcom/features/routing-forms/jsonLogicToPrisma"; +import { + getQueryBuilderConfigForFormFields, + type FormFieldsQueryBuilderConfigWithRaqbFields, +} from "@calcom/features/routing-forms/lib/getQueryBuilderConfig"; import { classNames } from "@calcom/lib"; import { downloadAsCsv, sanitizeValue } from "@calcom/lib/csvUtils"; import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver"; @@ -19,18 +30,7 @@ import { trpc } from "@calcom/trpc/react"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Button, showToast } from "@calcom/ui"; -import SingleForm, { - getServerSidePropsForSingleFormView as getServerSideProps, -} from "../../components/SingleForm"; -import { - withRaqbSettingsAndWidgets, - ConfigFor, -} from "../../components/react-awesome-query-builder/config/uiConfig"; -import type { JsonLogicQuery } from "../../jsonLogicToPrisma"; -import { - getQueryBuilderConfigForFormFields, - type FormFieldsQueryBuilderConfigWithRaqbFields, -} from "../../lib/getQueryBuilderConfig"; +import PageWrapper from "@components/PageWrapper"; export { getServerSideProps }; @@ -292,10 +292,5 @@ export default function ReporterWrapper({ ); } -ReporterWrapper.getLayout = (page: React.ReactElement) => { - return ( - - {page} - - ); -}; +ReporterWrapper.getLayout = getLayout; +ReporterWrapper.PageWrapper = PageWrapper; diff --git a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx b/apps/web/pages/routing/route-builder/[id].tsx similarity index 96% rename from packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx rename to apps/web/pages/routing/route-builder/[id].tsx index 75b1983f2b4f16..4f951fd0a7d9a5 100644 --- a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx +++ b/apps/web/pages/routing/route-builder/[id].tsx @@ -1,6 +1,7 @@ "use client"; import { useAutoAnimate } from "@formkit/auto-animate/react"; +import type { App_RoutingForms_Form } from "@prisma/client"; import type { Prisma } from "@prisma/client"; import Link from "next/link"; import React, { useCallback, useState, useEffect } from "react"; @@ -9,11 +10,39 @@ import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-bu import type { JsonTree } from "react-awesome-query-builder"; import type { UseFormReturn } from "react-hook-form"; -import Shell from "@calcom/features/shell/Shell"; +import { routingFormAppComponents } from "@calcom/features/routing-forms/appComponents"; +import DynamicAppComponent from "@calcom/features/routing-forms/components/DynamicAppComponent"; +import type { RoutingFormWithResponseCount } from "@calcom/features/routing-forms/components/SingleForm"; +import SingleForm from "@calcom/features/routing-forms/components/SingleForm"; +import { + withRaqbSettingsAndWidgets, + ConfigFor, +} from "@calcom/features/routing-forms/components/react-awesome-query-builder/config/uiConfig"; +import getLayout from "@calcom/features/routing-forms/getLayout"; +import { getServerSideProps } from "@calcom/features/routing-forms/getServerSidePropsSingleForm"; +import { RoutingPages } from "@calcom/features/routing-forms/lib/RoutingPages"; +import { createFallbackRoute } from "@calcom/features/routing-forms/lib/createFallbackRoute"; +import getEventTypeAppMetadata from "@calcom/features/routing-forms/lib/getEventTypeAppMetadata"; +import { + getQueryBuilderConfigForFormFields, + getQueryBuilderConfigForAttributes, + type FormFieldsQueryBuilderConfigWithRaqbFields, + type AttributesQueryBuilderConfigWithRaqbFields, +} from "@calcom/features/routing-forms/lib/getQueryBuilderConfig"; +import isRouter from "@calcom/features/routing-forms/lib/isRouter"; +import type { SerializableForm } from "@calcom/features/routing-forms/types/types"; +import type { + GlobalRoute, + LocalRoute, + SerializableRoute, + Attribute, + EditFormRoute, + AttributeRoutingConfig, +} from "@calcom/features/routing-forms/types/types"; +import { RouteActionType } from "@calcom/features/routing-forms/zod"; import { areTheySiblingEntitites } from "@calcom/lib/entityPermissionUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { buildEmptyQueryValue } from "@calcom/lib/raqb/raqbUtils"; -import type { App_RoutingForms_Form } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/client"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; @@ -28,36 +57,9 @@ import { Divider, } from "@calcom/ui"; -import { routingFormAppComponents } from "../../appComponents"; -import DynamicAppComponent from "../../components/DynamicAppComponent"; -import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; -import SingleForm, { - getServerSidePropsForSingleFormView as getServerSideProps, -} from "../../components/SingleForm"; -import { - withRaqbSettingsAndWidgets, - ConfigFor, -} from "../../components/react-awesome-query-builder/config/uiConfig"; -import { RoutingPages } from "../../lib/RoutingPages"; -import { createFallbackRoute } from "../../lib/createFallbackRoute"; -import getEventTypeAppMetadata from "../../lib/getEventTypeAppMetadata"; -import { - getQueryBuilderConfigForFormFields, - getQueryBuilderConfigForAttributes, - type FormFieldsQueryBuilderConfigWithRaqbFields, - type AttributesQueryBuilderConfigWithRaqbFields, -} from "../../lib/getQueryBuilderConfig"; -import isRouter from "../../lib/isRouter"; -import type { SerializableForm } from "../../types/types"; -import type { - GlobalRoute, - LocalRoute, - SerializableRoute, - Attribute, - EditFormRoute, - AttributeRoutingConfig, -} from "../../types/types"; -import { RouteActionType } from "../../zod"; +import PageWrapper from "@components/PageWrapper"; + +export { getServerSideProps }; type EventTypesByGroup = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]; @@ -1092,12 +1094,5 @@ export default function RouteBuilder({ ); } -RouteBuilder.getLayout = (page: React.ReactElement) => { - return ( - - {page} - - ); -}; - -export { getServerSideProps }; +RouteBuilder.getLayout = getLayout; +RouteBuilder.PageWrapper = PageWrapper; diff --git a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx b/apps/web/pages/routing/routing-link/[id].tsx similarity index 72% rename from packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx rename to apps/web/pages/routing/routing-link/[id].tsx index 42c983e4020da1..dc051cfadd020c 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/apps/web/pages/routing/routing-link/[id].tsx @@ -1,5 +1,6 @@ "use client"; +import type { GetServerSidePropsContext } from "next"; import Head from "next/head"; import { useRouter } from "next/navigation"; import type { FormEvent } from "react"; @@ -8,25 +9,30 @@ 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 FormInputFields from "@calcom/features/routing-forms/components/FormInputFields"; +import { enrichFormWithMigrationData } from "@calcom/features/routing-forms/enrichFormWithMigrationData"; +import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/features/routing-forms/getEventTypeRedirectUrl"; +import getFieldIdentifier from "@calcom/features/routing-forms/lib/getFieldIdentifier"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { getUrlSearchParamsToForward } from "@calcom/features/routing-forms/lib/getUrlSearchParamsToForward"; +import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm"; +import { findMatchingRoute } from "@calcom/features/routing-forms/lib/processRoute"; +import { substituteVariables } from "@calcom/features/routing-forms/lib/substituteVariables"; +import { getFieldResponseForJsonLogic } from "@calcom/features/routing-forms/lib/transformResponse"; +import type { NonRouterRoute, FormResponse } from "@calcom/features/routing-forms/types/types"; 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 { navigateInTopWindow } from "@calcom/lib/navigateInTopWindow"; +import { prisma } from "@calcom/prisma"; import { trpc } from "@calcom/trpc/react"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Button, showToast, useCalcomTheme } from "@calcom/ui"; -import FormInputFields from "../../components/FormInputFields"; -import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "../../getEventTypeRedirectUrl"; -import getFieldIdentifier from "../../lib/getFieldIdentifier"; -import { findMatchingRoute } from "../../lib/processRoute"; -import { substituteVariables } from "../../lib/substituteVariables"; -import { getFieldResponseForJsonLogic } from "../../lib/transformResponse"; -import type { NonRouterRoute, FormResponse } from "../../types/types"; -import { getServerSideProps } from "./getServerSideProps"; -import { getUrlSearchParamsToForward } from "./getUrlSearchParamsToForward"; +import { ssrInit } from "@server/lib/ssr"; type Props = inferSSRProps; const useBrandColors = ({ @@ -215,8 +221,6 @@ export default function RoutingLink(props: inferSSRProps { const searchParams = useCompatSearchParams(); const prefillResponse: FormResponse = {}; @@ -235,3 +239,88 @@ const usePrefilledResponse = (form: Props["form"]) => { const [response, setResponse] = useState(prefillResponse); return [response, setResponse] as const; }; + +export const getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { + const { params } = context; + if (!params?.formId) { + return { + notFound: true, + }; + } + const formId = params.formId as string; + const { currentOrgDomain } = orgDomainConfig(context.req); + + const isEmbed = context.query.embed === "true"; + if (context.query["flag.coep"] === "true") { + context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + } + + const form = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: formId, + }, + include: { + user: { + select: { + id: true, + movedToProfileId: true, + organization: { + select: { + slug: true, + }, + }, + username: true, + theme: true, + brandColor: true, + darkBrandColor: true, + metadata: true, + }, + }, + team: { + select: { + slug: true, + parent: { + select: { slug: true }, + }, + parentId: true, + metadata: true, + }, + }, + }, + }); + + if (!form || form.disabled) { + return { + notFound: true, + }; + } + + const { UserRepository } = await import("@calcom/lib/server/repository/user"); + const formWithUserProfile = { + ...form, + user: await UserRepository.enrichUserWithItsProfile({ user: form.user }), + }; + + if ( + !isAuthorizedToViewFormOnOrgDomain({ user: formWithUserProfile.user, currentOrgDomain, team: form.team }) + ) { + return { + notFound: true, + }; + } + + const ssr = await ssrInit(context); + return { + props: { + isEmbed, + themeBasis: form.user.username, + profile: { + theme: form.user.theme, + brandColor: form.user.brandColor, + darkBrandColor: form.user.darkBrandColor, + }, + form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }), + trpcState: ssr.dehydrate(), + }, + }; +}; diff --git a/apps/web/server/lib/ssr.ts b/apps/web/server/lib/ssr.ts index 11cf707e9c40f6..766fdc4dedbfb1 100644 --- a/apps/web/server/lib/ssr.ts +++ b/apps/web/server/lib/ssr.ts @@ -2,7 +2,6 @@ import type { GetServerSidePropsContext } from "next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import superjson from "superjson"; -import { forms } from "@calcom/app-store/routing-forms/trpc/procedures/forms"; import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { map } from "@calcom/features/flags/server/procedures/map"; import { CALCOM_VERSION } from "@calcom/lib/constants"; @@ -13,6 +12,7 @@ import { teamsAndUserProfilesQuery } from "@calcom/trpc/server/routers/loggedInV import { event } from "@calcom/trpc/server/routers/publicViewer/procedures/event"; import { session } from "@calcom/trpc/server/routers/publicViewer/procedures/session"; import { get } from "@calcom/trpc/server/routers/viewer/eventTypes/procedures/get"; +import { forms } from "@calcom/trpc/server/routers/viewer/routing-forms/procedures/forms"; import { hasTeamPlan } from "@calcom/trpc/server/routers/viewer/teams/procedures/hasTeamPlan"; import { router, mergeRouters } from "@calcom/trpc/server/trpc"; @@ -38,6 +38,9 @@ const routerSlice = router({ appRoutingForms: router({ forms, }), + routingForms: router({ + forms, + }), teamsAndUserProfilesQuery: router({ teamsAndUserProfilesQuery, }), diff --git a/apps/web/vercel.json b/apps/web/vercel.json index dc6e6899d06481..5b646f2bacd89f 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -21,7 +21,7 @@ "pages/api/trpc/highPerf/[trpc].ts": { "memory": 3008 }, - "pages/api/trpc/appRoutingForms/[trpc].ts": { + "pages/api/trpc/routingForms/[trpc].ts": { "memory": 2048 }, "pages/router/embed.tsx": { diff --git a/package.json b/package.json index c299b75dbf0e7a..179b976d6d470a 100644 --- a/package.json +++ b/package.json @@ -88,12 +88,15 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^16.0.1", "@types/dompurify": "^3.0.5", + "@types/fs-extra": "^11.0.4", "@types/jsdom": "^21.1.3", "@types/jsonwebtoken": "^9.0.3", "@vitest/ui": "^2.1.1", "c8": "^7.13.0", "checkly": "latest", "dotenv-checker": "^1.1.5", + "fs-extra": "^11.3.0", + "glob": "^11.0.1", "husky": "^8.0.0", "i18n-unused": "^0.13.0", "jest-diff": "^29.5.0", diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index becb7697c056aa..b795070bcb40b9 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -33,7 +33,6 @@ import { appKeysSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod"; import { appKeysSchema as plausible_zod_ts } from "./plausible/zod"; import { appKeysSchema as posthog_zod_ts } from "./posthog/zod"; import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod"; -import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod"; import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod"; import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod"; import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod"; @@ -83,7 +82,6 @@ export const appKeysSchemas = { plausible: plausible_zod_ts, posthog: posthog_zod_ts, qr_code: qr_code_zod_ts, - "routing-forms": routing_forms_zod_ts, salesforce: salesforce_zod_ts, shimmervideo: shimmervideo_zod_ts, stripe: stripepayment_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 9db12e052f93ea..90064b7b061b26 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -67,7 +67,6 @@ import raycast_config_json from "./raycast/config.json"; import retell_ai_config_json from "./retell-ai/config.json"; import riverside_config_json from "./riverside/config.json"; import roam_config_json from "./roam/config.json"; -import routing_forms_config_json from "./routing-forms/config.json"; import salesforce_config_json from "./salesforce/config.json"; import salesroom_config_json from "./salesroom/config.json"; import sendgrid_config_json from "./sendgrid/config.json"; @@ -170,7 +169,6 @@ export const appStoreMetadata = { "retell-ai": retell_ai_config_json, riverside: riverside_config_json, roam: roam_config_json, - "routing-forms": routing_forms_config_json, salesforce: salesforce_config_json, salesroom: salesroom_config_json, sendgrid: sendgrid_config_json, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 1acffdaeec7b35..26091eb67691c0 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -33,7 +33,6 @@ import { appDataSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod"; import { appDataSchema as plausible_zod_ts } from "./plausible/zod"; import { appDataSchema as posthog_zod_ts } from "./posthog/zod"; import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod"; -import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod"; import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod"; import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod"; import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod"; @@ -83,7 +82,6 @@ export const appDataSchemas = { plausible: plausible_zod_ts, posthog: posthog_zod_ts, qr_code: qr_code_zod_ts, - "routing-forms": routing_forms_zod_ts, salesforce: salesforce_zod_ts, shimmervideo: shimmervideo_zod_ts, stripe: stripepayment_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 4d81f334ff8a7f..79a8c403f8959e 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -67,7 +67,6 @@ export const apiHandlers = { "retell-ai": import("./retell-ai/api"), riverside: import("./riverside/api"), roam: import("./roam/api"), - "routing-forms": import("./routing-forms/api"), salesforce: import("./salesforce/api"), salesroom: import("./salesroom/api"), sendgrid: import("./sendgrid/api"), diff --git a/packages/app-store/routing-forms/DESCRIPTION.md b/packages/app-store/routing-forms/DESCRIPTION.md deleted file mode 100644 index 6084b5817608d6..00000000000000 --- a/packages/app-store/routing-forms/DESCRIPTION.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -items: - - 1.jpg - - 2.jpg - - 3.jpg ---- - -It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user diff --git a/packages/app-store/routing-forms/README.md b/packages/app-store/routing-forms/README.md deleted file mode 100644 index 53fafd2b64b904..00000000000000 --- a/packages/app-store/routing-forms/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Routing Forms App - -## Vocabulary -- RaqbField refers to the field in the context of React Awesome Query Builder(RAQB) - - See `getQueryBuilderConfig` -- Field refers to the field in the Routing Form - - -## Understanding the structure of queryValue(type JsonTree) - -### AttributeQueryValue -- { - id: "Some-id-generated-and-used-internally-by-raqb" - type: "group", - children1: { - "Some-other-id-generated-and-used-internally-by-raqb": { - type: "rule", - properties: { - field: "ATTRIBUTE.id", - operator: "multiselect_equals", // One of many operators possible - value: ["AttributeOption.id"], - // This is the type of the attribute and the operator corresponds to it. The data provided to jsonLogic on which the logic(generated from RAQB config and this queryValue) is applied must also be as per the requirement of the operator. - valueType: ["multiselect"] - } - } - } - -} - -## How to run Tests - -`yarn e2e:app-store` runs all Apps' tests. You can use `describe.only()` to run Routing Forms tests only. - -Make sure that the app is running already with NEXT_PUBLIC_IS_E2E=1 so that the app is installable diff --git a/packages/app-store/routing-forms/TODO.md b/packages/app-store/routing-forms/TODO.md deleted file mode 100644 index cf08213fc54355..00000000000000 --- a/packages/app-store/routing-forms/TODO.md +++ /dev/null @@ -1,53 +0,0 @@ -## TODO - Routing -- [ ] Switching from Select to MultiSelect in attribute - - [ ] It immediately makes the Routing logic to not work. Fix it. - - [ ] It removes the selected option from Routing. Fix it. - - [ ] Warnings are needed with recommendation to create new attribute instead in unsupported scenarios. We need to test for more such changes to Attribute types. -- [ ] Test Seated events as routing targets. -- Improvements - - [ ] When choosing 'Value of Field' option in RAQB, show a warning somewhere if all options of the Form field aren't present in the attribute option. - - So, if Form Field is 'What is Comapny Size' with options Enterprise, Large, Medium, Small and Attribute for which Value of 'What is compnay size' has options Enterprise, Large, Medium. It would show a warning that 'Small' option is not present in the attribute. - - [ ] When logic can't be built(Short text maps to an attribute with given set of options but the short text value doesn't match any of the options), the team member matching route throws error but Test Preview still shows the previous value - It show should the error in there. - - [ ] When routing to a team that doesn't have the team members assigned that are chosen, we are unable to select those users. FIX it by ignoring the assigned members(except the fixed hosts) and just using the attributes matched team members. -- [ ] Fetch bookings on window focus but not when rerouting dialog is open. As soon as it is closed, we need to refetch again. -- [ ] Seated Events - - [ ] Disable routing for seated events. Disable only during creation. -- [ ] When rescheduling to a new event, the booking title remains same which will most probably not match the event-type title. This is the behaviour of reschedule. Should we use the new event-type title? -- [ ] What params should we forward to the booking page that we show after rerouting -- [ ] On completing the rerouting just show the new Routing status -- [ ] We need to disallow changing the email identifier field as during a reschedule user isn't allowed to change their email currently. Allowing to change that email response would mean that we support changing email during reschedule which might be disabled for some reasons unknown yet. -- [ ] What if there are changes to Form before rerouting but after initial routing. -- [ ] Not able to prefill rescheduleReason due to a bug in useInitialFormValues hook -- [ ] It is becoming a requirement to have BookingAttempt record to allow to store reroutingFormResponses as well as teamMemberIds in it. Other query params could also be stored later on when needed. It is important to reduce the payload of the query params due to routedTeamMemberIds and reroutingFormResponses - - -### Performance Tests and Improvements -- [ ] Test with ~1000 team members and ~100 attributes. -- [ ] getAttributes query optimization. It needs info from various tables(Attribute, AttributeOption, AttributeToUser, Membership) -- [ ] findTeamMembersMatchingAttributesQuery - Parallelize jsonLogic.apply across team members. - -### Unit Tests -- [ ] evaluateRAQB - - [x] Basic Tests - - [ ] More Detailed Tests -- [ ] getAttributes - - [ ] Querying Logic Test -- [ ] getAvailableSlots - - [ ] should use routedTeamMemberIds in availability query - - -### Documentation / Tooltip -- [ ] Document well that the option label in Routing Form Field and Attribute Option label must be same to connect them. - - Due to the connection requirement b/w Attribute Option and Field Option, we use label(lowercased) to match attributes instead of attribute slug -- [ ] Fixed hosts of the event will be included through attribute routing as well. They aren't tested for attribute routing logic. -- [ ] When re-routed, the booking page doesn't allow rescheduling/cancellation to avoid any possible issues. We can open it up later. -- [ ] Booking with seats not supported yet. Need to figure out which seats should be re-routed or if re-routing makes sense for it even? -- [ ] Hashed link re-routing doesn't make sense with. With Routing you route to an event from form and it doesn't involve hashed link even if it is enabled -- [ ] User needs to ensure for now that the team event they redirect to has 'Assign all and future members of the team' checked. - -### V2.0 -- [ ] Fallback for when no team member matches the criteria. - - [x] Fallback will be attributes query builder that would match a different set of users. Though the booking will use the team members assigned to the event type, it might be better to be able to identify such a scenario and use a different set of users. It also makes it easy to identify when the fallback scenario happens. - - [ ] Mark if fallback was used by the router for a response. -- [ ] cal.routedTeamMembersIds query param - Could possible become a big payload and possibly break the URL limit. We could work on a short-lived row in a table that would hold that info and we pass the id of that row only in query param. handleNewBooking can then retrieve the routedTeamMembersIds from that short-lived row and delete the entry after successfully creating a booking. -- [ ] Better ability to test with contact owner from Routing Form Preview itself(if possible). Right now, we need to test the entire booking flow to verify that. \ No newline at end of file diff --git a/packages/app-store/routing-forms/appComponents.ts b/packages/app-store/routing-forms/appComponents.ts deleted file mode 100644 index 9c94b8df212b4e..00000000000000 --- a/packages/app-store/routing-forms/appComponents.ts +++ /dev/null @@ -1,5 +0,0 @@ -import dynamic from "next/dynamic"; - -export const routingFormAppComponents = { - salesforce: dynamic(() => import("../salesforce/components/RoutingFormOptions")), -}; diff --git a/packages/app-store/routing-forms/config.json b/packages/app-store/routing-forms/config.json deleted file mode 100644 index 22eacf80976013..00000000000000 --- a/packages/app-store/routing-forms/config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "/*": "Don't modify slug - If required, do it using cli edit command", - "name": "Routing Forms", - "title": "Routing Forms", - "isGlobal": true, - "slug": "routing-forms", - "type": "routing-forms_other", - "logo": "icon-dark.svg", - "url": "https://cal.com/resources/feature/routing-forms", - "variant": "other", - "categories": ["automation"], - "publisher": "Cal.com, Inc.", - "simplePath": "/apps/routing-forms", - "email": "help@cal.com", - "licenseRequired": true, - "teamsPlanRequired": { - "upgradeUrl": "/routing-forms/forms" - }, - "description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user", - "__createdUsingCli": true, - "isOAuth": false -} diff --git a/packages/app-store/routing-forms/index.ts b/packages/app-store/routing-forms/index.ts deleted file mode 100644 index d7f36022040096..00000000000000 --- a/packages/app-store/routing-forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as api from "./api"; diff --git a/packages/app-store/routing-forms/package.json b/packages/app-store/routing-forms/package.json deleted file mode 100644 index 1b6fec7222d1f5..00000000000000 --- a/packages/app-store/routing-forms/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "private": true, - "name": "@calcom/routing-forms", - "version": "0.0.0", - "main": "./index.ts", - "description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user ", - "dependencies": { - "@calcom/lib": "*", - "dotenv": "^16.3.1", - "json-logic-js": "^2.0.2", - "react-awesome-query-builder": "^5.1.2" - }, - "scripts": { - "lint": "eslint .", - "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix" - }, - "devDependencies": { - "@calcom/types": "*", - "@types/json-logic-js": "^1.2.1" - } -} diff --git a/packages/app-store/routing-forms/pages/app-routing.config.tsx b/packages/app-store/routing-forms/pages/app-routing.config.tsx deleted file mode 100644 index 4c322f1c21c4aa..00000000000000 --- a/packages/app-store/routing-forms/pages/app-routing.config.tsx +++ /dev/null @@ -1,35 +0,0 @@ -//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, - "route-builder": RouteBuilder, - forms: forms, - "routing-link": RoutingLink, - reporting: Reporting, - layoutHandler: LayoutHandler, - "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/layout-handler/[...appPages].tsx b/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx deleted file mode 100644 index 5d1058cb288cac..00000000000000 --- a/packages/app-store/routing-forms/pages/layout-handler/[...appPages].tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import type { GetServerSidePropsContext } from "next"; -import React from "react"; -import { FormProvider, useForm } from "react-hook-form"; - -import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; -import type { AppPrisma, AppSsrInit, AppUser } from "@calcom/types/AppGetServerSideProps"; - -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): Component => { - return (RoutingFormsRoutingConfig as unknown as Record)[route]; -}; - -export default function LayoutHandler(props: { [key: string]: unknown }) { - const params = useParamsWithFallback(); - const methods = useForm(); - const pageKey = Array.isArray(params.pages) - ? params.pages[0] - : params.pages?.split("/")[0] ?? DEFAULT_ROUTE; - const PageComponent = getComponent(pageKey).default; - return ( - - - - ); -} - -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); - } else { - return page; - } -}; - -export async function getServerSideProps( - context: GetServerSidePropsContext, - ...rest: GetServerSidePropsRestArgs -) { - const component = getComponent(context.params?.pages?.[0] || ""); - return component.getServerSideProps?.(context, ...rest) || { props: {} }; -} diff --git a/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts b/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts deleted file mode 100644 index a0671d861e14d4..00000000000000 --- a/packages/app-store/routing-forms/pages/routing-link/getServerSideProps.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm"; -import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps"; - -import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData"; -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 } = 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: { - id: formId, - }, - include: { - user: { - select: { - id: true, - movedToProfileId: true, - organization: { - select: { - slug: true, - }, - }, - username: true, - theme: true, - brandColor: true, - darkBrandColor: true, - metadata: true, - }, - }, - team: { - select: { - slug: true, - parent: { - select: { slug: true }, - }, - parentId: true, - metadata: true, - }, - }, - }, - }); - - if (!form || form.disabled) { - return { - notFound: true, - }; - } - - const { UserRepository } = await import("@calcom/lib/server/repository/user"); - const formWithUserProfile = { - ...form, - user: await UserRepository.enrichUserWithItsProfile({ user: form.user }), - }; - - if ( - !isAuthorizedToViewFormOnOrgDomain({ user: formWithUserProfile.user, currentOrgDomain, team: form.team }) - ) { - 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: enrichFormWithMigrationData(formWithUserProfile) }), - }, - }; -}; diff --git a/packages/app-store/salesforce/components/RoutingFormOptions.tsx b/packages/app-store/salesforce/components/RoutingFormOptions.tsx index 36760f07cbf7aa..4864f4225732b9 100644 --- a/packages/app-store/salesforce/components/RoutingFormOptions.tsx +++ b/packages/app-store/salesforce/components/RoutingFormOptions.tsx @@ -1,9 +1,12 @@ import { useState, useEffect } from "react"; +import type { + LocalRouteWithRaqbStates, + AttributeRoutingConfig, +} from "@calcom/features/routing-forms/types/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Select, Input, Label } from "@calcom/ui"; -import type { LocalRouteWithRaqbStates, AttributeRoutingConfig } from "../../routing-forms/types/types"; import { appDataSchema } from "../zod"; enum SalesforceRoutingConfig { diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index a2d61bb625ffcf..54d41d57156734 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -3,7 +3,7 @@ import jsforce from "@jsforce/jsforce-node"; import { RRule } from "rrule"; import { z } from "zod"; -import type { FormResponse } from "@calcom/app-store/routing-forms/types/types"; +import type { FormResponse } from "@calcom/features/routing-forms/types/types"; import { getLocation } from "@calcom/lib/CalEventParser"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { checkIfFreeEmailDomain } from "@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain"; diff --git a/packages/app-store/salesforce/lib/assignmentReasonHandler.ts b/packages/app-store/salesforce/lib/assignmentReasonHandler.ts index 60cf849b244401..44d9ad1497354b 100644 --- a/packages/app-store/salesforce/lib/assignmentReasonHandler.ts +++ b/packages/app-store/salesforce/lib/assignmentReasonHandler.ts @@ -1,4 +1,4 @@ -import { zodRoutes } from "@calcom/app-store/routing-forms/zod"; +import { zodRoutes } from "@calcom/features/routing-forms/zod"; import prisma from "@calcom/prisma"; import { AssignmentReasonEnum } from "@calcom/prisma/enums"; diff --git a/packages/app-store/typeform/playwright/tests/basic.e2e.ts b/packages/app-store/typeform/playwright/tests/basic.e2e.ts index 122b62774260de..c000045080afd8 100644 --- a/packages/app-store/typeform/playwright/tests/basic.e2e.ts +++ b/packages/app-store/typeform/playwright/tests/basic.e2e.ts @@ -4,7 +4,7 @@ import { expect } from "@playwright/test"; import { addForm as addRoutingForm, addOneFieldAndDescriptionAndSaveForm, -} from "@calcom/app-store/routing-forms/playwright/tests/testUtils"; +} from "@calcom/features/routing-forms/playwright/tests/testUtils"; import { WEBAPP_URL } from "@calcom/lib/constants"; import type { Fixtures } from "@calcom/web/playwright/lib/fixtures"; import { test } from "@calcom/web/playwright/lib/fixtures"; diff --git a/packages/emails/src/templates/index.ts b/packages/emails/src/templates/index.ts index 129c9ac89eba73..bdaa6bba9de6ad 100644 --- a/packages/emails/src/templates/index.ts +++ b/packages/emails/src/templates/index.ts @@ -28,7 +28,7 @@ export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelle export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail"; export { VerifyAccountEmail } from "./VerifyAccountEmail"; export { VerifyEmailByCode } from "./VerifyEmailByCode"; -export * from "@calcom/app-store/routing-forms/emails/components"; +export * from "@calcom/features/routing-forms/emails/components"; export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail"; export { DailyVideoDownloadTranscriptEmail } from "./DailyVideoDownloadTranscriptEmail"; export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail"; diff --git a/packages/features/Segment.tsx b/packages/features/Segment.tsx index 9f2ae2b1725ba7..26f749efab2500 100644 --- a/packages/features/Segment.tsx +++ b/packages/features/Segment.tsx @@ -8,8 +8,8 @@ import type { JsonTree } from "react-awesome-query-builder"; import { withRaqbSettingsAndWidgets, ConfigFor, -} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig"; -import { getQueryBuilderConfigForAttributes } from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig"; +} from "@calcom/features/routing-forms/components/react-awesome-query-builder/config/uiConfig"; +import { getQueryBuilderConfigForAttributes } from "@calcom/features/routing-forms/lib/getQueryBuilderConfig"; import { classNames as cn } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { isEqual } from "@calcom/lib/isEqual"; diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index 370e250ee196c2..92f477777cd8fb 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client"; import type short from "short-uuid"; import type { z } from "zod"; -import type { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod"; +import type { routingFormResponseInDbSchema } from "@calcom/features/routing-forms/zod"; import dayjs from "@calcom/dayjs"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import prisma from "@calcom/prisma"; diff --git a/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts b/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts index 65e39761295694..f9a7eb49455049 100644 --- a/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts +++ b/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts @@ -1,5 +1,5 @@ -import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types/types"; -import { zodRoutes } from "@calcom/app-store/routing-forms/zod"; +import type { FormResponse, Fields } from "@calcom/features/routing-forms/types/types"; +import { zodRoutes } from "@calcom/features/routing-forms/zod"; import { acrossQueryValueCompatiblity } from "@calcom/lib/raqb/raqbUtils"; import { getUsersAttributes } from "@calcom/lib/service/attribute/server/getAttributes"; import prisma from "@calcom/prisma"; diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index e90793d6d80fce..c5b025df26adad 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -4,8 +4,8 @@ import type { z } from "zod"; import type { SelectLikeComponentProps, TextLikeComponentProps, -} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets"; -import Widgets from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets"; +} from "@calcom/features/routing-forms/components/react-awesome-query-builder/widgets"; +import Widgets from "@calcom/features/routing-forms/components/react-awesome-query-builder/widgets"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AddressInput, diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index 7b82b4cfb05256..46ac529c499c4b 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -7,7 +7,7 @@ import startCase from "lodash/startCase"; import { zodFields as routingFormFieldsSchema, routingFormResponseInDbSchema, -} from "@calcom/app-store/routing-forms/zod"; +} from "@calcom/features/routing-forms/zod"; import dayjs from "@calcom/dayjs"; import type { ColumnFilter, TypedColumnFilter } from "@calcom/features/data-table"; import { ColumnFilterType } from "@calcom/features/data-table"; diff --git a/packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx b/packages/features/routing-forms/__tests__/TestFormDialog.test.tsx similarity index 100% rename from packages/app-store/routing-forms/__tests__/TestFormDialog.test.tsx rename to packages/features/routing-forms/__tests__/TestFormDialog.test.tsx diff --git a/packages/app-store/routing-forms/__tests__/config.test.ts b/packages/features/routing-forms/__tests__/config.test.ts similarity index 100% rename from packages/app-store/routing-forms/__tests__/config.test.ts rename to packages/features/routing-forms/__tests__/config.test.ts diff --git a/packages/app-store/routing-forms/__tests__/getEventTypeRedirectUrl.test.ts b/packages/features/routing-forms/__tests__/getEventTypeRedirectUrl.test.ts similarity index 100% rename from packages/app-store/routing-forms/__tests__/getEventTypeRedirectUrl.test.ts rename to packages/features/routing-forms/__tests__/getEventTypeRedirectUrl.test.ts diff --git a/packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts b/packages/features/routing-forms/__tests__/jsonLogicToPrisma.test.ts similarity index 100% rename from packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts rename to packages/features/routing-forms/__tests__/jsonLogicToPrisma.test.ts diff --git a/packages/app-store/routing-forms/__tests__/uiConfig.test.ts b/packages/features/routing-forms/__tests__/uiConfig.test.ts similarity index 100% rename from packages/app-store/routing-forms/__tests__/uiConfig.test.ts rename to packages/features/routing-forms/__tests__/uiConfig.test.ts diff --git a/packages/app-store/routing-forms/api/add.ts b/packages/features/routing-forms/api/add.ts similarity index 100% rename from packages/app-store/routing-forms/api/add.ts rename to packages/features/routing-forms/api/add.ts diff --git a/packages/app-store/routing-forms/api/index.ts b/packages/features/routing-forms/api/index.ts similarity index 100% rename from packages/app-store/routing-forms/api/index.ts rename to packages/features/routing-forms/api/index.ts diff --git a/packages/app-store/routing-forms/api/responses/[formId].ts b/packages/features/routing-forms/api/responses/[formId].ts similarity index 100% rename from packages/app-store/routing-forms/api/responses/[formId].ts rename to packages/features/routing-forms/api/responses/[formId].ts diff --git a/packages/app-store/routing-forms/appBookingFormHandler.ts b/packages/features/routing-forms/appBookingFormHandler.ts similarity index 79% rename from packages/app-store/routing-forms/appBookingFormHandler.ts rename to packages/features/routing-forms/appBookingFormHandler.ts index 0193f2226b095f..07e954009f90be 100644 --- a/packages/app-store/routing-forms/appBookingFormHandler.ts +++ b/packages/features/routing-forms/appBookingFormHandler.ts @@ -1,4 +1,5 @@ -import routingFormBookingFormHandler from "../salesforce/lib/routingFormBookingFormHandler"; +import routingFormBookingFormHandler from "@calcom/app-store/salesforce/lib/routingFormBookingFormHandler"; + import type { AttributeRoutingConfig } from "./types/types"; type AppBookingFormHandler = ( diff --git a/packages/features/routing-forms/appComponents.ts b/packages/features/routing-forms/appComponents.ts new file mode 100644 index 00000000000000..ccfb3b96488437 --- /dev/null +++ b/packages/features/routing-forms/appComponents.ts @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic"; + +export const routingFormAppComponents = { + salesforce: dynamic(() => import("@calcom/app-store/salesforce/components/RoutingFormOptions")), +}; diff --git a/packages/app-store/routing-forms/appDataSchemas.ts b/packages/features/routing-forms/appDataSchemas.ts similarity index 79% rename from packages/app-store/routing-forms/appDataSchemas.ts rename to packages/features/routing-forms/appDataSchemas.ts index bb9fa7197fd2ae..4731320cc415d6 100644 --- a/packages/app-store/routing-forms/appDataSchemas.ts +++ b/packages/features/routing-forms/appDataSchemas.ts @@ -1,4 +1,4 @@ -import { routingFormOptions as salesforce_routing_form_schema } from "../salesforce/zod"; +import { routingFormOptions as salesforce_routing_form_schema } from "@calcom/app-store/salesforce/zod"; export const routingFormAppDataSchemas = { salesforce: salesforce_routing_form_schema, diff --git a/packages/app-store/routing-forms/components/DynamicAppComponent.tsx b/packages/features/routing-forms/components/DynamicAppComponent.tsx similarity index 100% rename from packages/app-store/routing-forms/components/DynamicAppComponent.tsx rename to packages/features/routing-forms/components/DynamicAppComponent.tsx diff --git a/packages/app-store/routing-forms/components/FormActions.tsx b/packages/features/routing-forms/components/FormActions.tsx similarity index 99% rename from packages/app-store/routing-forms/components/FormActions.tsx rename to packages/features/routing-forms/components/FormActions.tsx index 46781698716e34..2cd635b38e4b7b 100644 --- a/packages/app-store/routing-forms/components/FormActions.tsx +++ b/packages/features/routing-forms/components/FormActions.tsx @@ -37,7 +37,7 @@ import { import getFieldIdentifier from "../lib/getFieldIdentifier"; import type { SerializableForm } from "../types/types"; -type RoutingForm = SerializableForm; +export type RoutingForm = SerializableForm; const newFormModalQuerySchema = z.object({ action: z.literal("new").or(z.literal("duplicate")), diff --git a/packages/app-store/routing-forms/components/FormInputFields.tsx b/packages/features/routing-forms/components/FormInputFields.tsx similarity index 100% rename from packages/app-store/routing-forms/components/FormInputFields.tsx rename to packages/features/routing-forms/components/FormInputFields.tsx diff --git a/packages/app-store/routing-forms/components/InfoLostWarningDialog.tsx b/packages/features/routing-forms/components/InfoLostWarningDialog.tsx similarity index 100% rename from packages/app-store/routing-forms/components/InfoLostWarningDialog.tsx rename to packages/features/routing-forms/components/InfoLostWarningDialog.tsx diff --git a/packages/app-store/routing-forms/components/RoutingNavBar.tsx b/packages/features/routing-forms/components/RoutingNavBar.tsx similarity index 100% rename from packages/app-store/routing-forms/components/RoutingNavBar.tsx rename to packages/features/routing-forms/components/RoutingNavBar.tsx diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/features/routing-forms/components/SingleForm.tsx similarity index 99% rename from packages/app-store/routing-forms/components/SingleForm.tsx rename to packages/features/routing-forms/components/SingleForm.tsx index a7aeeae80e2a17..84186d44b1f93d 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/features/routing-forms/components/SingleForm.tsx @@ -1,4 +1,4 @@ -import type { App_RoutingForms_Form, Team } from "@prisma/client"; +import type { Team } from "@prisma/client"; import Link from "next/link"; import { useEffect, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; @@ -38,17 +38,16 @@ import { } from "@calcom/ui"; import { getAbsoluteEventTypeRedirectUrl } from "../getEventTypeRedirectUrl"; +import type { getServerSideProps as getServerSidePropsForSingleFormView } from "../getServerSidePropsSingleForm"; import { RoutingPages } from "../lib/RoutingPages"; import { isFallbackRoute } from "../lib/isFallbackRoute"; import { findMatchingRoute } from "../lib/processRoute"; -import type { FormResponse, NonRouterRoute, SerializableForm } from "../types/types"; +import type { FormResponse, NonRouterRoute } from "../types/types"; +import type { RoutingForm } from "./FormActions"; import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions"; import FormInputFields from "./FormInputFields"; import { InfoLostWarningDialog } from "./InfoLostWarningDialog"; import RoutingNavBar from "./RoutingNavBar"; -import { getServerSidePropsForSingleFormView } from "./getServerSidePropsSingleForm"; - -type RoutingForm = SerializableForm; export type RoutingFormWithResponseCount = RoutingForm & { team: { @@ -999,5 +998,3 @@ export default function SingleFormWrapper({ form: _form, ...props }: SingleFormC ); } - -export { getServerSidePropsForSingleFormView }; diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts b/packages/features/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts rename to packages/features/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.ts b/packages/features/routing-forms/components/react-awesome-query-builder/config/config.ts similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/config/config.ts rename to packages/features/routing-forms/components/react-awesome-query-builder/config/config.ts diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/types.ts b/packages/features/routing-forms/components/react-awesome-query-builder/config/types.ts similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/config/types.ts rename to packages/features/routing-forms/components/react-awesome-query-builder/config/types.ts diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig.tsx b/packages/features/routing-forms/components/react-awesome-query-builder/config/uiConfig.tsx similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig.tsx rename to packages/features/routing-forms/components/react-awesome-query-builder/config/uiConfig.tsx diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.test.tsx b/packages/features/routing-forms/components/react-awesome-query-builder/widgets.test.tsx similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.test.tsx rename to packages/features/routing-forms/components/react-awesome-query-builder/widgets.test.tsx diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx b/packages/features/routing-forms/components/react-awesome-query-builder/widgets.tsx similarity index 100% rename from packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx rename to packages/features/routing-forms/components/react-awesome-query-builder/widgets.tsx diff --git a/packages/app-store/routing-forms/emails/components/ResponseEmail.tsx b/packages/features/routing-forms/emails/components/ResponseEmail.tsx similarity index 100% rename from packages/app-store/routing-forms/emails/components/ResponseEmail.tsx rename to packages/features/routing-forms/emails/components/ResponseEmail.tsx diff --git a/packages/app-store/routing-forms/emails/components/index.tsx b/packages/features/routing-forms/emails/components/index.tsx similarity index 100% rename from packages/app-store/routing-forms/emails/components/index.tsx rename to packages/features/routing-forms/emails/components/index.tsx diff --git a/packages/app-store/routing-forms/emails/templates/response-email.ts b/packages/features/routing-forms/emails/templates/response-email.ts similarity index 100% rename from packages/app-store/routing-forms/emails/templates/response-email.ts rename to packages/features/routing-forms/emails/templates/response-email.ts diff --git a/packages/app-store/routing-forms/enrichFormWithMigrationData.ts b/packages/features/routing-forms/enrichFormWithMigrationData.ts similarity index 100% rename from packages/app-store/routing-forms/enrichFormWithMigrationData.ts rename to packages/features/routing-forms/enrichFormWithMigrationData.ts diff --git a/packages/app-store/routing-forms/env.d.ts b/packages/features/routing-forms/env.d.ts similarity index 100% rename from packages/app-store/routing-forms/env.d.ts rename to packages/features/routing-forms/env.d.ts diff --git a/packages/app-store/routing-forms/getEventTypeRedirectUrl.ts b/packages/features/routing-forms/getEventTypeRedirectUrl.ts similarity index 100% rename from packages/app-store/routing-forms/getEventTypeRedirectUrl.ts rename to packages/features/routing-forms/getEventTypeRedirectUrl.ts diff --git a/packages/features/routing-forms/getLayout.tsx b/packages/features/routing-forms/getLayout.tsx new file mode 100644 index 00000000000000..53cac5a017f0f1 --- /dev/null +++ b/packages/features/routing-forms/getLayout.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { FormProvider, useForm } from "react-hook-form"; + +import Shell from "@calcom/features/shell/Shell"; + +export default function Layout(page: React.ReactNode) { + const methods = useForm(); + return ( + + + {page} + + + ); +} diff --git a/packages/app-store/routing-forms/pages/forms/getServerSideProps.ts b/packages/features/routing-forms/getServerSidePropsForms.ts similarity index 60% rename from packages/app-store/routing-forms/pages/forms/getServerSideProps.ts rename to packages/features/routing-forms/getServerSidePropsForms.ts index 345c0b3e5f0395..9bdb570021fff9 100644 --- a/packages/app-store/routing-forms/pages/forms/getServerSideProps.ts +++ b/packages/features/routing-forms/getServerSidePropsForms.ts @@ -1,18 +1,15 @@ +import type { GetServerSidePropsContext } from "next"; + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; 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) { +import { ssrInit } from "@server/lib/ssr"; + +export const getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { + const { req } = context; + const session = await getServerSession({ req }); + + if (!session?.user?.id) { return { redirect: { permanent: false, @@ -20,9 +17,9 @@ export const getServerSideProps = async function getServerSideProps( }, }; } - const ssr = await ssrInit(context); const filters = getTeamsFiltersFromQuery(context.query); + const ssr = await ssrInit(context); await ssr.viewer.appRoutingForms.forms.prefetch({ filters, @@ -31,7 +28,8 @@ export const getServerSideProps = async function getServerSideProps( await ssr.viewer.teamsAndUserProfilesQuery.prefetch(); return { props: { - trpcState: await ssr.dehydrate(), + appUrl: "/routing", + trpcState: ssr.dehydrate(), }, }; }; diff --git a/packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts b/packages/features/routing-forms/getServerSidePropsSingleForm.ts similarity index 72% rename from packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts rename to packages/features/routing-forms/getServerSidePropsSingleForm.ts index 9b1b633144bb06..4dee066bc722bc 100644 --- a/packages/app-store/routing-forms/components/getServerSidePropsSingleForm.ts +++ b/packages/features/routing-forms/getServerSidePropsSingleForm.ts @@ -1,20 +1,17 @@ -import type { - AppGetServerSidePropsContext, - AppPrisma, - AppSsrInit, - AppUser, -} from "@calcom/types/AppGetServerSideProps"; +import type { GetServerSidePropsContext } from "next"; -import { enrichFormWithMigrationData } from "../enrichFormWithMigrationData"; -import { getSerializableForm } from "../lib/getSerializableForm"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { prisma } from "@calcom/prisma"; -export const getServerSidePropsForSingleFormView = async function getServerSidePropsForSingleFormView( - context: AppGetServerSidePropsContext, - prisma: AppPrisma, - user: AppUser, - ssrInit: AppSsrInit -) { +import { ssrInit } from "@server/lib/ssr"; + +import { enrichFormWithMigrationData } from "./enrichFormWithMigrationData"; +import { getSerializableForm } from "./lib/getSerializableForm"; + +export async function getServerSideProps(context: GetServerSidePropsContext) { const ssr = await ssrInit(context); + const session = await getServerSession({ req: context.req }); + const user = session?.user; if (!user) { return { @@ -25,19 +22,15 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP }; } const { params } = context; - if (!params) { - return { - notFound: true, - }; - } - const formId = params.appPages[0]; - if (!formId || params.appPages.length > 1) { + if (!params?.id || typeof params.id !== "string") { return { notFound: true, }; } - const isFormCreateEditAllowed = (await import("../lib/isFormCreateEditAllowed")).isFormCreateEditAllowed; + const formId = params.id; + + const isFormCreateEditAllowed = (await import("./lib/isFormCreateEditAllowed")).isFormCreateEditAllowed; if (!(await isFormCreateEditAllowed({ userId: user.id, formId, targetTeamId: null }))) { return { redirect: { @@ -86,6 +79,7 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP }, }, }); + if (!form) { return { notFound: true, @@ -113,6 +107,7 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP return { props: { + appUrl: "/routing", trpcState: await ssr.dehydrate(), form: await getSerializableForm({ form: formWithoutProfileInfo }), enrichedWithUserProfileForm: await getSerializableForm({ @@ -120,4 +115,4 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP }), }, }; -}; +} diff --git a/packages/app-store/routing-forms/jsonLogicToPrisma.ts b/packages/features/routing-forms/jsonLogicToPrisma.ts similarity index 100% rename from packages/app-store/routing-forms/jsonLogicToPrisma.ts rename to packages/features/routing-forms/jsonLogicToPrisma.ts diff --git a/packages/app-store/routing-forms/lib/FieldTypes.ts b/packages/features/routing-forms/lib/FieldTypes.ts similarity index 100% rename from packages/app-store/routing-forms/lib/FieldTypes.ts rename to packages/features/routing-forms/lib/FieldTypes.ts diff --git a/packages/app-store/routing-forms/lib/InitialConfig.tsx b/packages/features/routing-forms/lib/InitialConfig.tsx similarity index 100% rename from packages/app-store/routing-forms/lib/InitialConfig.tsx rename to packages/features/routing-forms/lib/InitialConfig.tsx diff --git a/packages/app-store/routing-forms/lib/RoutingPages.ts b/packages/features/routing-forms/lib/RoutingPages.ts similarity index 100% rename from packages/app-store/routing-forms/lib/RoutingPages.ts rename to packages/features/routing-forms/lib/RoutingPages.ts diff --git a/packages/app-store/routing-forms/lib/__tests__/getQueryBuilderConfig.test.ts b/packages/features/routing-forms/lib/__tests__/getQueryBuilderConfig.test.ts similarity index 100% rename from packages/app-store/routing-forms/lib/__tests__/getQueryBuilderConfig.test.ts rename to packages/features/routing-forms/lib/__tests__/getQueryBuilderConfig.test.ts diff --git a/packages/app-store/routing-forms/lib/constants.ts b/packages/features/routing-forms/lib/constants.ts similarity index 100% rename from packages/app-store/routing-forms/lib/constants.ts rename to packages/features/routing-forms/lib/constants.ts diff --git a/packages/app-store/routing-forms/lib/createFallbackRoute.ts b/packages/features/routing-forms/lib/createFallbackRoute.ts similarity index 100% rename from packages/app-store/routing-forms/lib/createFallbackRoute.ts rename to packages/features/routing-forms/lib/createFallbackRoute.ts diff --git a/packages/app-store/routing-forms/lib/enabledApps.ts b/packages/features/routing-forms/lib/enabledApps.ts similarity index 100% rename from packages/app-store/routing-forms/lib/enabledApps.ts rename to packages/features/routing-forms/lib/enabledApps.ts diff --git a/packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts b/packages/features/routing-forms/lib/enabledIncompleteBookingApps.ts similarity index 100% rename from packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts rename to packages/features/routing-forms/lib/enabledIncompleteBookingApps.ts diff --git a/packages/app-store/routing-forms/lib/getConnectedForms.ts b/packages/features/routing-forms/lib/getConnectedForms.ts similarity index 100% rename from packages/app-store/routing-forms/lib/getConnectedForms.ts rename to packages/features/routing-forms/lib/getConnectedForms.ts diff --git a/packages/app-store/routing-forms/lib/getEventTypeAppMetadata.ts b/packages/features/routing-forms/lib/getEventTypeAppMetadata.ts similarity index 100% rename from packages/app-store/routing-forms/lib/getEventTypeAppMetadata.ts rename to packages/features/routing-forms/lib/getEventTypeAppMetadata.ts diff --git a/packages/app-store/routing-forms/lib/getFieldIdentifier.ts b/packages/features/routing-forms/lib/getFieldIdentifier.ts similarity index 100% rename from packages/app-store/routing-forms/lib/getFieldIdentifier.ts rename to packages/features/routing-forms/lib/getFieldIdentifier.ts diff --git a/packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts b/packages/features/routing-forms/lib/getQueryBuilderConfig.ts similarity index 100% rename from packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts rename to packages/features/routing-forms/lib/getQueryBuilderConfig.ts diff --git a/packages/app-store/routing-forms/lib/getSerializableForm.ts b/packages/features/routing-forms/lib/getSerializableForm.ts similarity index 96% rename from packages/app-store/routing-forms/lib/getSerializableForm.ts rename to packages/features/routing-forms/lib/getSerializableForm.ts index df6d669154e029..31d709b56c71ad 100644 --- a/packages/app-store/routing-forms/lib/getSerializableForm.ts +++ b/packages/features/routing-forms/lib/getSerializableForm.ts @@ -14,6 +14,8 @@ import isRouter from "./isRouter"; import isRouterLinkedField from "./isRouterLinkedField"; import { getFieldWithOptions } from "./selectOptions"; +type ZodRoutesView = typeof zodRoutesView; +type ZodFieldsView = typeof zodFieldsView; const log = logger.getSubLogger({ prefix: ["getSerializableForm"] }); /** * Doesn't have deleted fields by default @@ -48,7 +50,7 @@ export async function getSerializableForm({ const parsedFields = (withDeletedFields ? fieldsParsed.data : fieldsParsed.data?.filter((f) => !f.deleted)) || []; const parsedRoutes = routesParsed.data; - const fields = parsedFields as NonNullable>; + const fields = parsedFields as NonNullable>; const fieldsExistInForm: Record = {}; parsedFields.forEach((f) => { @@ -105,7 +107,7 @@ export async function getSerializableForm({ */ async function getEnrichedRoutesAndRouters(parsedRoutes: z.infer, userId: number) { const routers: { name: string; description: string | null; id: string }[] = []; - const routes: z.infer = []; + const routes: z.infer = []; if (!parsedRoutes) { return { routes, routers }; } diff --git a/packages/app-store/routing-forms/lib/getServerTimingHeader.ts b/packages/features/routing-forms/lib/getServerTimingHeader.ts similarity index 100% rename from packages/app-store/routing-forms/lib/getServerTimingHeader.ts rename to packages/features/routing-forms/lib/getServerTimingHeader.ts diff --git a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.test.ts b/packages/features/routing-forms/lib/getUrlSearchParamsToForward.test.ts similarity index 100% rename from packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.test.ts rename to packages/features/routing-forms/lib/getUrlSearchParamsToForward.test.ts diff --git a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts b/packages/features/routing-forms/lib/getUrlSearchParamsToForward.ts similarity index 93% rename from packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts rename to packages/features/routing-forms/lib/getUrlSearchParamsToForward.ts index 0a8eb8332f7316..4a7d07a4564565 100644 --- a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts +++ b/packages/features/routing-forms/lib/getUrlSearchParamsToForward.ts @@ -1,9 +1,11 @@ +import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/features/routing-forms/lib/constants"; +import getFieldIdentifier from "@calcom/features/routing-forms/lib/getFieldIdentifier"; +import type { FormResponse, LocalRoute } from "@calcom/features/routing-forms/types/types"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import type { getServerSideProps } from "@calcom/web/pages/routing/routing-link/[id]"; -import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "../../lib/constants"; -import getFieldIdentifier from "../../lib/getFieldIdentifier"; -import type { FormResponse, LocalRoute } from "../../types/types"; -import type { getServerSideProps } from "./getServerSideProps"; +type A = LocalRoute; +type B = typeof getServerSideProps; type FormResponseValueOnly = { [key: string]: { value: FormResponse[keyof FormResponse]["value"] } }; type Props = inferSSRProps; diff --git a/packages/app-store/routing-forms/lib/handleResponse.ts b/packages/features/routing-forms/lib/handleResponse.ts similarity index 100% rename from packages/app-store/routing-forms/lib/handleResponse.ts rename to packages/features/routing-forms/lib/handleResponse.ts diff --git a/packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts b/packages/features/routing-forms/lib/incompleteBooking/actionDataSchemas.ts similarity index 100% rename from packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts rename to packages/features/routing-forms/lib/incompleteBooking/actionDataSchemas.ts diff --git a/packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts b/packages/features/routing-forms/lib/incompleteBooking/actionFunctions.ts similarity index 100% rename from packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts rename to packages/features/routing-forms/lib/incompleteBooking/actionFunctions.ts diff --git a/packages/app-store/routing-forms/lib/isFallbackRoute.ts b/packages/features/routing-forms/lib/isFallbackRoute.ts similarity index 100% rename from packages/app-store/routing-forms/lib/isFallbackRoute.ts rename to packages/features/routing-forms/lib/isFallbackRoute.ts diff --git a/packages/app-store/routing-forms/lib/isFormCreateEditAllowed.ts b/packages/features/routing-forms/lib/isFormCreateEditAllowed.ts similarity index 100% rename from packages/app-store/routing-forms/lib/isFormCreateEditAllowed.ts rename to packages/features/routing-forms/lib/isFormCreateEditAllowed.ts diff --git a/packages/app-store/routing-forms/lib/isRouter.ts b/packages/features/routing-forms/lib/isRouter.ts similarity index 100% rename from packages/app-store/routing-forms/lib/isRouter.ts rename to packages/features/routing-forms/lib/isRouter.ts diff --git a/packages/app-store/routing-forms/lib/isRouterLinkedField.ts b/packages/features/routing-forms/lib/isRouterLinkedField.ts similarity index 100% rename from packages/app-store/routing-forms/lib/isRouterLinkedField.ts rename to packages/features/routing-forms/lib/isRouterLinkedField.ts diff --git a/packages/app-store/routing-forms/lib/processRoute.tsx b/packages/features/routing-forms/lib/processRoute.tsx similarity index 100% rename from packages/app-store/routing-forms/lib/processRoute.tsx rename to packages/features/routing-forms/lib/processRoute.tsx diff --git a/packages/app-store/routing-forms/lib/reportingUtils.ts b/packages/features/routing-forms/lib/reportingUtils.ts similarity index 100% rename from packages/app-store/routing-forms/lib/reportingUtils.ts rename to packages/features/routing-forms/lib/reportingUtils.ts diff --git a/packages/app-store/routing-forms/lib/selectOptions.ts b/packages/features/routing-forms/lib/selectOptions.ts similarity index 100% rename from packages/app-store/routing-forms/lib/selectOptions.ts rename to packages/features/routing-forms/lib/selectOptions.ts diff --git a/packages/app-store/routing-forms/lib/substituteVariables.ts b/packages/features/routing-forms/lib/substituteVariables.ts similarity index 100% rename from packages/app-store/routing-forms/lib/substituteVariables.ts rename to packages/features/routing-forms/lib/substituteVariables.ts diff --git a/packages/app-store/routing-forms/lib/transformResponse.test.ts b/packages/features/routing-forms/lib/transformResponse.test.ts similarity index 100% rename from packages/app-store/routing-forms/lib/transformResponse.test.ts rename to packages/features/routing-forms/lib/transformResponse.test.ts diff --git a/packages/app-store/routing-forms/lib/transformResponse.ts b/packages/features/routing-forms/lib/transformResponse.ts similarity index 100% rename from packages/app-store/routing-forms/lib/transformResponse.ts rename to packages/features/routing-forms/lib/transformResponse.ts diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/features/routing-forms/playwright/tests/basic.e2e.ts similarity index 100% rename from packages/app-store/routing-forms/playwright/tests/basic.e2e.ts rename to packages/features/routing-forms/playwright/tests/basic.e2e.ts diff --git a/packages/app-store/routing-forms/playwright/tests/testUtils.ts b/packages/features/routing-forms/playwright/tests/testUtils.ts similarity index 100% rename from packages/app-store/routing-forms/playwright/tests/testUtils.ts rename to packages/features/routing-forms/playwright/tests/testUtils.ts diff --git a/packages/app-store/routing-forms/static/1.jpg b/packages/features/routing-forms/static/1.jpg similarity index 100% rename from packages/app-store/routing-forms/static/1.jpg rename to packages/features/routing-forms/static/1.jpg diff --git a/packages/app-store/routing-forms/static/2.jpg b/packages/features/routing-forms/static/2.jpg similarity index 100% rename from packages/app-store/routing-forms/static/2.jpg rename to packages/features/routing-forms/static/2.jpg diff --git a/packages/app-store/routing-forms/static/3.jpg b/packages/features/routing-forms/static/3.jpg similarity index 100% rename from packages/app-store/routing-forms/static/3.jpg rename to packages/features/routing-forms/static/3.jpg diff --git a/packages/app-store/routing-forms/static/icon-dark.svg b/packages/features/routing-forms/static/icon-dark.svg similarity index 100% rename from packages/app-store/routing-forms/static/icon-dark.svg rename to packages/features/routing-forms/static/icon-dark.svg diff --git a/packages/app-store/routing-forms/trpc-router.ts b/packages/features/routing-forms/trpc-router.ts similarity index 100% rename from packages/app-store/routing-forms/trpc-router.ts rename to packages/features/routing-forms/trpc-router.ts diff --git a/packages/app-store/routing-forms/trpc/_router.ts b/packages/features/routing-forms/trpc/_router.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/_router.ts rename to packages/features/routing-forms/trpc/_router.ts diff --git a/packages/app-store/routing-forms/trpc/buildResponsesForReporting.test.ts b/packages/features/routing-forms/trpc/buildResponsesForReporting.test.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/buildResponsesForReporting.test.ts rename to packages/features/routing-forms/trpc/buildResponsesForReporting.test.ts diff --git a/packages/app-store/routing-forms/trpc/deleteForm.handler.ts b/packages/features/routing-forms/trpc/deleteForm.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/deleteForm.handler.ts rename to packages/features/routing-forms/trpc/deleteForm.handler.ts diff --git a/packages/app-store/routing-forms/trpc/deleteForm.schema.ts b/packages/features/routing-forms/trpc/deleteForm.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/deleteForm.schema.ts rename to packages/features/routing-forms/trpc/deleteForm.schema.ts diff --git a/packages/app-store/routing-forms/trpc/formMutation.handler.ts b/packages/features/routing-forms/trpc/formMutation.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/formMutation.handler.ts rename to packages/features/routing-forms/trpc/formMutation.handler.ts diff --git a/packages/app-store/routing-forms/trpc/formMutation.schema.ts b/packages/features/routing-forms/trpc/formMutation.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/formMutation.schema.ts rename to packages/features/routing-forms/trpc/formMutation.schema.ts diff --git a/packages/app-store/routing-forms/trpc/formQuery.handler.ts b/packages/features/routing-forms/trpc/formQuery.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/formQuery.handler.ts rename to packages/features/routing-forms/trpc/formQuery.handler.ts diff --git a/packages/app-store/routing-forms/trpc/formQuery.schema.ts b/packages/features/routing-forms/trpc/formQuery.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/formQuery.schema.ts rename to packages/features/routing-forms/trpc/formQuery.schema.ts diff --git a/packages/app-store/routing-forms/trpc/forms.handler.ts b/packages/features/routing-forms/trpc/forms.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/forms.handler.ts rename to packages/features/routing-forms/trpc/forms.handler.ts diff --git a/packages/app-store/routing-forms/trpc/forms.schema.ts b/packages/features/routing-forms/trpc/forms.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/forms.schema.ts rename to packages/features/routing-forms/trpc/forms.schema.ts diff --git a/packages/app-store/routing-forms/trpc/getAttributesForTeam.handler.ts b/packages/features/routing-forms/trpc/getAttributesForTeam.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/getAttributesForTeam.handler.ts rename to packages/features/routing-forms/trpc/getAttributesForTeam.handler.ts diff --git a/packages/app-store/routing-forms/trpc/getAttributesForTeam.schema.ts b/packages/features/routing-forms/trpc/getAttributesForTeam.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/getAttributesForTeam.schema.ts rename to packages/features/routing-forms/trpc/getAttributesForTeam.schema.ts diff --git a/packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.handler.ts b/packages/features/routing-forms/trpc/getIncompleteBookingSettings.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.handler.ts rename to packages/features/routing-forms/trpc/getIncompleteBookingSettings.handler.ts diff --git a/packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.schema.ts b/packages/features/routing-forms/trpc/getIncompleteBookingSettings.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.schema.ts rename to packages/features/routing-forms/trpc/getIncompleteBookingSettings.schema.ts diff --git a/packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts b/packages/features/routing-forms/trpc/getResponseWithFormFields.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts rename to packages/features/routing-forms/trpc/getResponseWithFormFields.handler.ts diff --git a/packages/app-store/routing-forms/trpc/procedures/forms.ts b/packages/features/routing-forms/trpc/procedures/forms.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/procedures/forms.ts rename to packages/features/routing-forms/trpc/procedures/forms.ts diff --git a/packages/app-store/routing-forms/trpc/report.handler.ts b/packages/features/routing-forms/trpc/report.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/report.handler.ts rename to packages/features/routing-forms/trpc/report.handler.ts diff --git a/packages/app-store/routing-forms/trpc/report.schema.ts b/packages/features/routing-forms/trpc/report.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/report.schema.ts rename to packages/features/routing-forms/trpc/report.schema.ts diff --git a/packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.handler.ts b/packages/features/routing-forms/trpc/saveIncompleteBookingSettings.handler.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.handler.ts rename to packages/features/routing-forms/trpc/saveIncompleteBookingSettings.handler.ts diff --git a/packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.schema.ts b/packages/features/routing-forms/trpc/saveIncompleteBookingSettings.schema.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.schema.ts rename to packages/features/routing-forms/trpc/saveIncompleteBookingSettings.schema.ts diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/features/routing-forms/trpc/utils.ts similarity index 100% rename from packages/app-store/routing-forms/trpc/utils.ts rename to packages/features/routing-forms/trpc/utils.ts diff --git a/packages/app-store/routing-forms/types/types.d.ts b/packages/features/routing-forms/types/types.d.ts similarity index 100% rename from packages/app-store/routing-forms/types/types.d.ts rename to packages/features/routing-forms/types/types.d.ts diff --git a/packages/app-store/routing-forms/zod.ts b/packages/features/routing-forms/zod.ts similarity index 100% rename from packages/app-store/routing-forms/zod.ts rename to packages/features/routing-forms/zod.ts diff --git a/packages/features/shell/navigation/Navigation.tsx b/packages/features/shell/navigation/Navigation.tsx index 385cc0f0912514..be3f7420b676bf 100644 --- a/packages/features/shell/navigation/Navigation.tsx +++ b/packages/features/shell/navigation/Navigation.tsx @@ -57,7 +57,7 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy icon: "grid-3x3", isCurrent: ({ pathname: path, item }) => { // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) - return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false); + return path?.startsWith(item.href) ?? false; }, child: [ { @@ -65,11 +65,7 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy href: "/apps", isCurrent: ({ pathname: path, item }) => { // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) - return ( - (path?.startsWith(item.href) ?? false) && - !(path?.includes("routing-forms/") ?? false) && - !(path?.includes("/installed") ?? false) - ); + return (path?.startsWith(item.href) ?? false) && !(path?.includes("/installed") ?? false); }, }, { @@ -88,9 +84,9 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy }, { name: "routing_forms", - href: "/apps/routing-forms/forms", + href: "/routing/forms", icon: "file-text", - isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false, + isCurrent: ({ pathname }) => pathname?.startsWith("/routing/") ?? false, moreOnMobile: true, }, { diff --git a/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook.ts b/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook.ts index e50e538a865744..dab3b682efb7ba 100644 --- a/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook.ts +++ b/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook.ts @@ -1,7 +1,7 @@ import { z } from "zod"; -import incompleteBookingActionFunctions from "@calcom/app-store/routing-forms/lib/incompleteBooking/actionFunctions"; -import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/trpc/utils"; +import incompleteBookingActionFunctions from "@calcom/features/routing-forms/lib/incompleteBooking/actionFunctions"; +import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/features/routing-forms/trpc/utils"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; import prisma from "@calcom/prisma"; diff --git a/packages/lib/raqb/findTeamMembersMatchingAttributeLogic.test.ts b/packages/lib/raqb/findTeamMembersMatchingAttributeLogic.test.ts index eee677d752c832..27cf9f6ccacd32 100644 --- a/packages/lib/raqb/findTeamMembersMatchingAttributeLogic.test.ts +++ b/packages/lib/raqb/findTeamMembersMatchingAttributeLogic.test.ts @@ -1,7 +1,7 @@ import type { BaseWidget } from "react-awesome-query-builder"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; +import { RouteActionType } from "@calcom/features/routing-forms/zod"; import { RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic"; import { findTeamMembersMatchingAttributeLogic, diff --git a/packages/lib/raqb/raqbUtils.ts b/packages/lib/raqb/raqbUtils.ts index 5b72019717abd8..a81a4a0a3b7b51 100644 --- a/packages/lib/raqb/raqbUtils.ts +++ b/packages/lib/raqb/raqbUtils.ts @@ -2,8 +2,8 @@ import type { JsonGroup, JsonItem, JsonRule, JsonTree } from "react-awesome-quer import type { Config } from "react-awesome-query-builder"; import { Utils as QbUtils } from "react-awesome-query-builder"; -import { getQueryBuilderConfigForAttributes } from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig"; -import type { LocalRoute } from "@calcom/app-store/routing-forms/types/types"; +import { getQueryBuilderConfigForAttributes } from "@calcom/features/routing-forms/lib/getQueryBuilderConfig"; +import type { LocalRoute } from "@calcom/features/routing-forms/types/types"; import logger from "@calcom/lib/logger"; import type { dynamicFieldValueOperands, dynamicFieldValueOperandsResponse } from "@calcom/lib/raqb/types"; import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; diff --git a/packages/lib/server/getLuckyUser.test.ts b/packages/lib/server/getLuckyUser.test.ts index f2d56d212b612b..12ed78d32c44b7 100644 --- a/packages/lib/server/getLuckyUser.test.ts +++ b/packages/lib/server/getLuckyUser.test.ts @@ -13,7 +13,7 @@ import { getLuckyUser, prepareQueuesAndAttributesData } from "./getLuckyUser"; type NonEmptyArray = [T, ...T[]]; type GetLuckyUserAvailableUsersType = NonEmptyArray>; -vi.mock("@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets", () => ({ +vi.mock("@calcom/features/routing-forms/components/react-awesome-query-builder/widgets", () => ({ default: {}, })); vi.mock("@calcom/ui", () => ({})); diff --git a/packages/lib/server/getLuckyUser.ts b/packages/lib/server/getLuckyUser.ts index f71aa7dae570a1..e096b39e647fe7 100644 --- a/packages/lib/server/getLuckyUser.ts +++ b/packages/lib/server/getLuckyUser.ts @@ -1,7 +1,7 @@ import type { Prisma, User } from "@prisma/client"; -import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types/types"; -import { zodRoutes } from "@calcom/app-store/routing-forms/zod"; +import type { FormResponse, Fields } from "@calcom/features/routing-forms/types/types"; +import { zodRoutes } from "@calcom/features/routing-forms/zod"; import { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; import dayjs from "@calcom/dayjs"; import logger from "@calcom/lib/logger"; diff --git a/packages/lib/server/getRoutedUrl.ts b/packages/lib/server/getRoutedUrl.ts index 6b1794d10c7eab..a573281592f020 100644 --- a/packages/lib/server/getRoutedUrl.ts +++ b/packages/lib/server/getRoutedUrl.ts @@ -3,19 +3,19 @@ import type { GetServerSidePropsContext } from "next"; import { stringify } from "querystring"; import z from "zod"; -import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData"; -import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl"; -import getFieldIdentifier from "@calcom/app-store/routing-forms/lib/getFieldIdentifier"; -import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm"; -import { getServerTimingHeader } from "@calcom/app-store/routing-forms/lib/getServerTimingHeader"; -import { handleResponse } from "@calcom/app-store/routing-forms/lib/handleResponse"; -import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute"; -import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables"; -import { getFieldResponseForJsonLogic } from "@calcom/app-store/routing-forms/lib/transformResponse"; -import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward"; -import type { FormResponse } from "@calcom/app-store/routing-forms/types/types"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { enrichFormWithMigrationData } from "@calcom/features/routing-forms/enrichFormWithMigrationData"; +import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/features/routing-forms/getEventTypeRedirectUrl"; +import getFieldIdentifier from "@calcom/features/routing-forms/lib/getFieldIdentifier"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { getServerTimingHeader } from "@calcom/features/routing-forms/lib/getServerTimingHeader"; +import { getUrlSearchParamsToForward } from "@calcom/features/routing-forms/lib/getUrlSearchParamsToForward"; +import { handleResponse } from "@calcom/features/routing-forms/lib/handleResponse"; import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm"; +import { findMatchingRoute } from "@calcom/features/routing-forms/lib/processRoute"; +import { substituteVariables } from "@calcom/features/routing-forms/lib/substituteVariables"; +import { getFieldResponseForJsonLogic } from "@calcom/features/routing-forms/lib/transformResponse"; +import type { FormResponse } from "@calcom/features/routing-forms/types/types"; import logger from "@calcom/lib/logger"; import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm"; import { TRPCError } from "@calcom/trpc/server"; diff --git a/packages/lib/server/repository/booking.ts b/packages/lib/server/repository/booking.ts index 941c2c5abe7c7b..ee38503a4252b6 100644 --- a/packages/lib/server/repository/booking.ts +++ b/packages/lib/server/repository/booking.ts @@ -1,6 +1,6 @@ import type { Prisma } from "@prisma/client"; -import type { FormResponse } from "@calcom/app-store/routing-forms/types/types"; +import type { FormResponse } from "@calcom/features/routing-forms/types/types"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { Booking } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/lib/server/repository/routingForm.ts b/packages/lib/server/repository/routingForm.ts index 4284df24ddb716..4b8e44b10ab37d 100644 --- a/packages/lib/server/repository/routingForm.ts +++ b/packages/lib/server/repository/routingForm.ts @@ -6,7 +6,19 @@ export class RoutingFormRepository { where: { id: formId, }, - include: { + select: { + id: true, + name: true, + description: true, + disabled: true, + fields: true, + routes: true, + createdAt: true, + updatedAt: true, + userId: true, + teamId: true, + position: true, + settings: true, user: { select: { id: true, @@ -14,6 +26,9 @@ export class RoutingFormRepository { email: true, movedToProfileId: true, metadata: true, + theme: true, + brandColor: true, + darkBrandColor: true, organization: { select: { slug: true, diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index df77a7244707a7..1339cd0b0ea058 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -13,7 +13,7 @@ import type { } from "zod"; import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; -import { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod"; +import { routingFormResponseInDbSchema } from "@calcom/features/routing-forms/zod"; import dayjs from "@calcom/dayjs"; import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema"; diff --git a/packages/trpc/server/routers/loggedInViewer/workflowOrder.handler.ts b/packages/trpc/server/routers/loggedInViewer/workflowOrder.handler.ts index ce5093e389f688..579be671232742 100644 --- a/packages/trpc/server/routers/loggedInViewer/workflowOrder.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/workflowOrder.handler.ts @@ -1,4 +1,4 @@ -import type { TFormSchema } from "@calcom/app-store/routing-forms/trpc/forms.schema"; +import type { TFormSchema } from "@calcom/features/routing-forms/trpc/forms.schema"; import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; diff --git a/packages/trpc/server/routers/viewer/_router.tsx b/packages/trpc/server/routers/viewer/_router.tsx index f0eaaee1cd1bfb..c589c7814d6c50 100644 --- a/packages/trpc/server/routers/viewer/_router.tsx +++ b/packages/trpc/server/routers/viewer/_router.tsx @@ -1,5 +1,4 @@ import app_Basecamp3 from "@calcom/app-store/basecamp3/trpc-router"; -import app_RoutingForms from "@calcom/app-store/routing-forms/trpc-router"; import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router"; import { featureFlagRouter } from "@calcom/features/flags/server/router"; import { insightsRouter } from "@calcom/features/insights/server/trpc-router"; @@ -54,9 +53,6 @@ export const viewerRouter = mergeRouters( dsync: dsyncRouter, insights: insightsRouter, payments: paymentsRouter, - // NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved. - // After that there would just one merge call here for all the apps. - appRoutingForms: app_RoutingForms, appBasecamp3: app_Basecamp3, features: featureFlagRouter, appsRouter, @@ -66,6 +62,9 @@ export const viewerRouter = mergeRouters( admin: adminRouter, attributes: attributesRouter, highPerf: highPerfRouter, + // Backward compatibility with appRoutingForms but prefer `routingForms` i.e. use viewer.routingForms and not viewer.appRoutingForms for any new code. + // Switching from viewer.appRoutingForms to viewer.routingForms is as simple as changing the name but would require changes across a ton of files. + appRoutingForms: routingFormsRouter, routingForms: routingFormsRouter, }) ); diff --git a/packages/trpc/server/routers/viewer/routing-forms/_router.ts b/packages/trpc/server/routers/viewer/routing-forms/_router.ts index a41ccccee8751f..5849d3b9fbeeae 100644 --- a/packages/trpc/server/routers/viewer/routing-forms/_router.ts +++ b/packages/trpc/server/routers/viewer/routing-forms/_router.ts @@ -1,14 +1,88 @@ +import { z } from "zod"; + import authedProcedure from "../../../procedures/authedProcedure"; import publicProcedure from "../../../procedures/publicProcedure"; import { router, importHandler } from "../../../trpc"; +import { ZDeleteFormInputSchema } from "./deleteForm.schema"; import { ZFindTeamMembersMatchingAttributeLogicOfRouteInputSchema } from "./findTeamMembersMatchingAttributeLogicOfRoute.schema"; +import { ZFormMutationInputSchema } from "./formMutation.schema"; +import { ZFormQueryInputSchema } from "./formQuery.schema"; +import { ZGetAttributesForTeamInputSchema } from "./getAttributesForTeam.schema"; +import { ZGetIncompleteBookingSettingsInputSchema } from "./getIncompleteBookingSettings.schema"; +import { forms } from "./procedures/forms"; +import { ZReportInputSchema } from "./report.schema"; import { ZResponseInputSchema } from "./response.schema"; +import { ZSaveIncompleteBookingSettingsInputSchema } from "./saveIncompleteBookingSettings.schema"; const NAMESPACE = "routingForms"; const namespaced = (s: string) => `${NAMESPACE}.${s}`; +export const ZFormByResponseIdInputSchema = z.object({ + formResponseId: z.number(), +}); + +export type TFormQueryInputSchema = z.infer; + export const routingFormsRouter = router({ + forms, + formQuery: authedProcedure.input(ZFormQueryInputSchema).query(async ({ ctx, input }) => { + const handler = await importHandler(namespaced("formQuery"), () => import("./formQuery.handler")); + return handler({ ctx, input }); + }), + getResponseWithFormFields: authedProcedure + .input(ZFormByResponseIdInputSchema) + .query(async ({ ctx, input }) => { + const handler = await importHandler( + namespaced("getResponseWithFormFields"), + () => import("./getResponseWithFormFields.handler") + ); + return handler({ ctx, input }); + }), + formMutation: authedProcedure.input(ZFormMutationInputSchema).mutation(async ({ ctx, input }) => { + const handler = await importHandler(namespaced("formMutation"), () => import("./formMutation.handler")); + return handler({ ctx, input }); + }), + deleteForm: authedProcedure.input(ZDeleteFormInputSchema).mutation(async ({ ctx, input }) => { + const handler = await importHandler(namespaced("deleteForm"), () => import("./deleteForm.handler")); + return handler({ ctx, input }); + }), + + report: authedProcedure.input(ZReportInputSchema).query(async ({ ctx, input }) => { + const handler = await importHandler(namespaced("report"), () => import("./report.handler")); + return handler({ ctx, input }); + }), + + getAttributesForTeam: authedProcedure + .input(ZGetAttributesForTeamInputSchema) + .query(async ({ ctx, input }) => { + const handler = await importHandler( + namespaced("getAttributesForTeam"), + () => import("./getAttributesForTeam.handler") + ); + return handler({ ctx, input }); + }), + + getIncompleteBookingSettings: authedProcedure + .input(ZGetIncompleteBookingSettingsInputSchema) + .query(async ({ ctx, input }) => { + const handler = await importHandler( + namespaced("getIncompleteBookingSettings"), + () => import("./getIncompleteBookingSettings.handler") + ); + return handler({ ctx, input }); + }), + + saveIncompleteBookingSettings: authedProcedure + .input(ZSaveIncompleteBookingSettingsInputSchema) + .mutation(async ({ ctx, input }) => { + const handler = await importHandler( + namespaced("saveIncompleteBookingSettings"), + () => import("./saveIncompleteBookingSettings.handler") + ); + return handler({ ctx, input }); + }), + findTeamMembersMatchingAttributeLogicOfRoute: authedProcedure .input(ZFindTeamMembersMatchingAttributeLogicOfRouteInputSchema) .mutation(async ({ ctx, input }) => { @@ -26,3 +100,5 @@ export const routingFormsRouter = router({ }), }), }); + +export default routingFormsRouter; diff --git a/packages/trpc/server/routers/viewer/routing-forms/buildResponsesForReporting.test.ts b/packages/trpc/server/routers/viewer/routing-forms/buildResponsesForReporting.test.ts new file mode 100644 index 00000000000000..fb107cb8684bdc --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/buildResponsesForReporting.test.ts @@ -0,0 +1,158 @@ +import { v4 as uuidv4 } from "uuid"; +import { describe, it, expect } from "vitest"; + +import { buildResponsesForReporting } from "./report.handler"; + +describe("buildResponsesForReporting", () => { + it("for fields with options, it should return the labels of the options", () => { + const field1Id = uuidv4(); + const field2Id = uuidv4(); + + const responsesFromDb = [ + { + [field1Id]: { value: "value1" }, + [field2Id]: { value: ["option1", "option2"] }, + }, + ]; + + const fields = [ + { id: field1Id, label: "Field 1" }, + { + id: field2Id, + label: "Field 2", + options: [ + { id: "option1", label: "Option 1" }, + { id: "option2", label: "Option 2" }, + ], + }, + ]; + + const expectedResponses = [["value1", "Option 1, Option 2"]]; + + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(["Field 1", "Field 2"]); + }); + + it("for fields with options having null id, it should return what DB provided(as that would be labels only).", () => { + const field1Id = uuidv4(); + const field2Id = uuidv4(); + const responsesFromDb = [ + { + [field1Id]: { value: "value1" }, + [field2Id]: { value: ["Option 1", "Option 2"] }, + }, + ]; + + const fields = [ + { id: field1Id, label: "Field 1" }, + { + id: field2Id, + label: "Field 2", + options: [ + { id: null, label: "Option 1" }, + { id: null, label: "Option 2" }, + ], + }, + ]; + + const expectedResponses = [["value1", "Option 1, Option 2"]]; + + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(["Field 1", "Field 2"]); + }); + + it("for fields with no options but having array value, it should return the value as is", () => { + const field2Id = uuidv4(); + const responsesFromDb = [ + { + [field2Id]: { value: ["Option 1", "Option 2"] }, + }, + ]; + + const fields = [ + { + id: field2Id, + label: "Field 2", + }, + ]; + + const expectedResponses = [["Option 1, Option 2"]]; + + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(["Field 2"]); + }); + + it("should correctly handle numeric responses converting them to strings", () => { + const field1Id = uuidv4(); + const field2Id = uuidv4(); + const responsesFromDb = [ + { + [field1Id]: { value: 1 }, + [field2Id]: { value: [1, 2] }, + }, + ]; + + const fields = [ + { id: field1Id, label: "Field 1" }, + { + id: field2Id, + label: "Field 2", + options: [ + { id: null, label: "Option 1" }, + { id: null, label: "Option 2" }, + ], + }, + ]; + const expectedResponses = [["1", "1, 2"]]; + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(["Field 1", "Field 2"]); + }); + + it("should handle empty responses", () => { + const field1Id = uuidv4(); + const responsesFromDb = [{}]; + + const fields = [{ id: field1Id, label: "Field 1" }]; + + const expectedResponses = [[""]]; + + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(["Field 1"]); + }); + + it("should show correct header for deleted fields", () => { + const field1Id = uuidv4(); + const field2Id = uuidv4(); + const responsesFromDb = [ + { + [field1Id]: { value: "value1" }, + [field2Id]: { value: ["Option 1", "Option 2"] }, + }, + ]; + + const fields = [ + { id: field1Id, label: "Field 1" }, + { + id: field2Id, + label: "Field 2", + deleted: true, + options: [ + { id: "option1", label: "Option 1" }, + { id: "option2", label: "Option 2" }, + ], + }, + ]; + + const expectedResponses = [["value1", "Option 1, Option 2"]]; + const expectedHeaders = ["Field 1", "Field 2(Deleted)"]; + + const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); + expect(responses).toEqual(expectedResponses); + expect(headers).toEqual(expectedHeaders); + }); +}); diff --git a/packages/trpc/server/routers/viewer/routing-forms/deleteForm.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/deleteForm.handler.ts new file mode 100644 index 00000000000000..f49655d2522443 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/deleteForm.handler.ts @@ -0,0 +1,55 @@ +import getConnectedForms from "@calcom/features/routing-forms/lib/getConnectedForms"; +import { isFormCreateEditAllowed } from "@calcom/features/routing-forms/lib/isFormCreateEditAllowed"; +import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; +import type { PrismaClient } from "@calcom/prisma"; +import { TRPCError } from "@calcom/trpc/server"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TDeleteFormInputSchema } from "./deleteForm.schema"; + +interface DeleteFormHandlerOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TDeleteFormInputSchema; +} +export const deleteFormHandler = async ({ ctx, input }: DeleteFormHandlerOptions) => { + const { user, prisma } = ctx; + if (!(await isFormCreateEditAllowed({ userId: user.id, formId: input.id, targetTeamId: null }))) { + throw new TRPCError({ + code: "FORBIDDEN", + }); + } + + const areFormsUsingIt = ( + await getConnectedForms(prisma, { + id: input.id, + userId: user.id, + }) + ).length; + + if (areFormsUsingIt) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "This form is being used by other forms. Please remove it's usage from there first.", + }); + } + + const deletedRes = await prisma.app_RoutingForms_Form.deleteMany({ + where: { + id: input.id, + ...entityPrismaWhereClause({ userId: user.id }), + }, + }); + + if (!deletedRes.count) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Form seems to be already deleted.", + }); + } + return deletedRes; +}; + +export default deleteFormHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/deleteForm.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/deleteForm.schema.ts new file mode 100644 index 00000000000000..8e3bab6a0e60d8 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/deleteForm.schema.ts @@ -0,0 +1,7 @@ +import z from "zod"; + +export const ZDeleteFormInputSchema = z.object({ + id: z.string(), +}); + +export type TDeleteFormInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts index 218f86f3053329..c189ce11a544d2 100644 --- a/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts +++ b/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts @@ -6,8 +6,8 @@ import type { App_RoutingForms_Form } from "@prisma/client"; import type { ServerResponse } from "http"; import type { NextApiResponse } from "next"; -import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData"; -import { getUrlSearchParamsToForwardForTestPreview } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward"; +import { enrichFormWithMigrationData } from "@calcom/features/routing-forms/enrichFormWithMigrationData"; +import { getUrlSearchParamsToForwardForTestPreview } from "@calcom/features/routing-forms/lib/getUrlSearchParamsToForward"; import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; import { fromEntriesWithDuplicateKeys } from "@calcom/lib/fromEntriesWithDuplicateKeys"; import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic"; @@ -25,6 +25,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { TFindTeamMembersMatchingAttributeLogicOfRouteInputSchema } from "./findTeamMembersMatchingAttributeLogicOfRoute.schema"; +type A = TrpcSessionUser; interface FindTeamMembersMatchingAttributeLogicOfRouteHandlerOptions { ctx: { prisma: PrismaClient; diff --git a/packages/trpc/server/routers/viewer/routing-forms/formMutation.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/formMutation.handler.ts new file mode 100644 index 00000000000000..4e5adcbdfd038f --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/formMutation.handler.ts @@ -0,0 +1,392 @@ +import type { App_RoutingForms_Form } from "@prisma/client"; +import { Prisma } from "@prisma/client"; + +import { createFallbackRoute } from "@calcom/features/routing-forms/lib/createFallbackRoute"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { isFallbackRoute } from "@calcom/features/routing-forms/lib/isFallbackRoute"; +import { isFormCreateEditAllowed } from "@calcom/features/routing-forms/lib/isFormCreateEditAllowed"; +import isRouter from "@calcom/features/routing-forms/lib/isRouter"; +import isRouterLinkedField from "@calcom/features/routing-forms/lib/isRouterLinkedField"; +import type { SerializableForm } from "@calcom/features/routing-forms/types/types"; +import { zodFields, zodRouterRoute, zodRoutes } from "@calcom/features/routing-forms/zod"; +import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils"; +import type { PrismaClient } from "@calcom/prisma"; +import { TRPCError } from "@calcom/trpc/server"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TFormMutationInputSchema } from "./formMutation.schema"; + +interface FormMutationHandlerOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TFormMutationInputSchema; +} + +export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOptions) => { + const { user, prisma } = ctx; + const { name, id, description, disabled, addFallback, duplicateFrom, shouldConnect } = input; + let teamId = input.teamId; + const settings = input.settings; + if (!(await isFormCreateEditAllowed({ userId: user.id, formId: id, targetTeamId: teamId }))) { + throw new TRPCError({ + code: "FORBIDDEN", + }); + } + + let { routes: inputRoutes, fields: inputFields } = input; + + inputFields = inputFields || []; + inputRoutes = inputRoutes || []; + type InputFields = typeof inputFields; + type InputRoutes = typeof inputRoutes; + let routes: InputRoutes; + let fields: InputFields; + type DuplicateFrom = NonNullable; + + const dbForm = await prisma.app_RoutingForms_Form.findUnique({ + where: { + id: id, + }, + select: { + id: true, + user: true, + name: true, + description: true, + userId: true, + disabled: true, + createdAt: true, + updatedAt: true, + routes: true, + fields: true, + settings: true, + teamId: true, + position: true, + }, + }); + + const dbSerializedForm = dbForm + ? await getSerializableForm({ form: dbForm, withDeletedFields: true }) + : null; + + if (duplicateFrom) { + ({ teamId, routes, fields } = await getRoutesAndFieldsForDuplication({ duplicateFrom, userId: user.id })); + } else { + [fields, routes] = [inputFields, inputRoutes]; + if (dbSerializedForm) { + fields = markMissingFieldsDeleted(dbSerializedForm, fields); + } + } + + if (dbSerializedForm) { + // If it's an existing form being mutated, update fields in the connected forms(if any). + await updateFieldsInConnectedForms(dbSerializedForm, inputFields); + } + + fields = await getUpdatedRouterLinkedFields(fields, routes); + + if (addFallback) { + // Add a fallback route if there is none + if (!routes.find(isFallbackRoute)) { + routes.push(createFallbackRoute()); + } + } + + // Validate the users passed + if (teamId && settings?.sendUpdatesTo?.length) { + const sendUpdatesTo = await prisma.membership.findMany({ + where: { + teamId, + userId: { + in: settings.sendUpdatesTo, + }, + }, + select: { + userId: true, + }, + }); + settings.sendUpdatesTo = sendUpdatesTo.map((member) => member.userId); + // If its not a team, the user is sending the value, we will just ignore it + } else if (!teamId && settings?.sendUpdatesTo) { + delete settings.sendUpdatesTo; + } + + return await prisma.app_RoutingForms_Form.upsert({ + where: { + id: id, + }, + create: { + user: { + connect: { + id: user.id, + }, + }, + fields, + name: name, + description, + // Prisma doesn't allow setting null value directly for JSON. It recommends using JsonNull for that case. + routes: routes === null ? Prisma.JsonNull : routes, + id: id, + ...(teamId + ? { + team: { + connect: { + id: teamId ?? undefined, + }, + }, + } + : null), + }, + update: { + disabled: disabled, + fields, + name: name, + description, + settings: settings === null ? Prisma.JsonNull : settings, + routes: routes === null ? Prisma.JsonNull : routes, + }, + }); + + /** + * If Form has Router Linked fields, enrich them with the latest info from the Router + * If Form doesn't have Router fields but there is a Router used in routes, add all the fields from the Router + */ + async function getUpdatedRouterLinkedFields(fields: InputFields, routes: InputRoutes) { + const routerLinkedFields: Record = {}; + for (const [, field] of Object.entries(fields)) { + if (!isRouterLinkedField(field)) { + continue; + } + routerLinkedFields[field.routerId] = true; + + if (!routes.some((route) => route.id === field.routerId)) { + // If the field is from a router that is not available anymore, mark it as deleted + field.deleted = true; + continue; + } + // Get back deleted field as now the Router is there for it. + if (field.deleted) field.deleted = false; + const router = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: field.routerId, + userId: user.id, + }, + }); + if (router) { + assertIfInvalidRouter(router); + const parsedRouterFields = zodFields.parse(router.fields); + + // There is a field from some router available, make sure that the field has up-to-date info from the router + const routerField = parsedRouterFields?.find((f) => f.id === field.id); + // Update local field(cache) with router field on every mutation + Object.assign(field, routerField); + } + } + + for (const [, route] of Object.entries(routes)) { + if (!isRouter(route)) { + continue; + } + + // If there is a field that belongs to router, then all fields must be there already. So, need to add Router fields + if (routerLinkedFields[route.id]) { + continue; + } + + const router = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: route.id, + userId: user.id, + }, + }); + if (router) { + assertIfInvalidRouter(router); + const parsedRouterFields = zodFields.parse(router.fields); + const fieldsFromRouter = parsedRouterFields + ?.filter((f) => !f.deleted) + .map((f) => { + return { + ...f, + routerId: route.id, + }; + }); + + if (fieldsFromRouter) { + fields = fields.concat(fieldsFromRouter); + } + } + } + return fields; + } + + function findFieldWithId(id: string, fields: InputFields) { + return fields.find((field) => field.id === id); + } + + /** + * Update fields in connected forms as per the inputFields + */ + async function updateFieldsInConnectedForms( + serializedForm: SerializableForm, + inputFields: InputFields + ) { + for (const [, connectedForm] of Object.entries(serializedForm.connectedForms)) { + const connectedFormDb = await prisma.app_RoutingForms_Form.findFirst({ + where: { + id: connectedForm.id, + }, + }); + if (!connectedFormDb) { + continue; + } + const connectedFormFields = zodFields.parse(connectedFormDb.fields); + + const fieldsThatAreNotInConnectedForm = ( + inputFields?.filter((f) => !findFieldWithId(f.id, connectedFormFields || [])) || [] + ).map((f) => ({ + ...f, + routerId: serializedForm.id, + })); + + const updatedConnectedFormFields = connectedFormFields + // Update fields that are already in connected form + ?.map((field) => { + if (isRouterLinkedField(field) && field.routerId === serializedForm.id) { + return { + ...field, + ...findFieldWithId(field.id, inputFields || []), + }; + } + return field; + }) + // Add fields that are not there + .concat(fieldsThatAreNotInConnectedForm); + + await prisma.app_RoutingForms_Form.update({ + where: { + id: connectedForm.id, + }, + data: { + fields: updatedConnectedFormFields, + }, + }); + } + } + + async function getRoutesAndFieldsForDuplication({ + duplicateFrom, + userId, + }: { + duplicateFrom: DuplicateFrom; + userId: number; + }) { + const sourceForm = await prisma.app_RoutingForms_Form.findFirst({ + where: { + ...entityPrismaWhereClause({ userId }), + id: duplicateFrom, + }, + select: { + id: true, + fields: true, + routes: true, + userId: true, + teamId: true, + team: { + select: { + id: true, + members: true, + }, + }, + }, + }); + + if (!sourceForm) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Form to duplicate: ${duplicateFrom} not found`, + }); + } + + if (!(await canEditEntity(sourceForm, userId))) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Form to duplicate: ${duplicateFrom} not found or you are unauthorized`, + }); + } + + //TODO: Instead of parsing separately, use getSerializableForm. That would automatically remove deleted fields as well. + const fieldsParsed = zodFields.safeParse(sourceForm.fields); + const routesParsed = zodRoutes.safeParse(sourceForm.routes); + if (!fieldsParsed.success || !routesParsed.success) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Could not parse source form's fields or routes", + }); + } + + let fields, routes; + if (shouldConnect) { + routes = [ + // This connected route would automatically link the fields + zodRouterRoute.parse({ + id: sourceForm.id, + isRouter: true, + }), + ]; + fields = + fieldsParsed.data + // Deleted fields in the form shouldn't be added to the new form + ?.filter((f) => !f.deleted) + .map((f) => { + return { + id: f.id, + routerId: sourceForm.id, + label: "", + type: "", + }; + }) || []; + } else { + // Duplicate just routes and fields + // We don't want name, description and responses to be copied + routes = routesParsed.data || []; + // FIXME: Deleted fields shouldn't come in duplicate + fields = fieldsParsed.data ? fieldsParsed.data.filter((f) => !f.deleted) : []; + } + return { teamId: sourceForm.teamId, routes, fields }; + } + + function markMissingFieldsDeleted( + serializedForm: SerializableForm, + fields: InputFields + ) { + // Find all fields that are in DB(including deleted) but not in the mutation + // e.g. inputFields is [A,B,C]. DB is [A,B,C,D,E,F]. It means D,E,F got deleted + const deletedFields = + serializedForm.fields?.filter((f) => !fields.find((field) => field.id === f.id)) || []; + + // Add back deleted fields in the end and mark them deleted. + // Fields mustn't be deleted, to make sure columns never decrease which hugely simplifies CSV generation + fields = fields.concat( + deletedFields.map((f) => { + f.deleted = true; + return f; + }) + ); + return fields; + } + function assertIfInvalidRouter(router: App_RoutingForms_Form) { + const routesOfRouter = zodRoutes.parse(router.routes); + if (routesOfRouter) { + if (routesOfRouter.find(isRouter)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "A form being used as a Router must be a Origin form. It must not be using any other Router.", + }); + } + } + } +}; + +export default formMutationHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/formMutation.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/formMutation.schema.ts new file mode 100644 index 00000000000000..32167c3ff94c52 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/formMutation.schema.ts @@ -0,0 +1,20 @@ +import z from "zod"; + +import { zodFields, zodRoutes } from "@calcom/features/routing-forms/zod"; +import { RoutingFormSettings } from "@calcom/prisma/zod-utils"; + +export const ZFormMutationInputSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + disabled: z.boolean().optional(), + fields: zodFields, + routes: zodRoutes, + addFallback: z.boolean().optional(), + duplicateFrom: z.string().nullable().optional(), + teamId: z.number().nullish().default(null), + shouldConnect: z.boolean().optional(), + settings: RoutingFormSettings.optional(), +}); + +export type TFormMutationInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/formQuery.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/formQuery.handler.ts new file mode 100644 index 00000000000000..642d82a232c32c --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/formQuery.handler.ts @@ -0,0 +1,44 @@ +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; +import type { PrismaClient } from "@calcom/prisma"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TFormQueryInputSchema } from "./formQuery.schema"; + +interface FormsHandlerOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TFormQueryInputSchema; +} + +export const formQueryHandler = async ({ ctx, input }: FormsHandlerOptions) => { + const { prisma, user } = ctx; + const form = await prisma.app_RoutingForms_Form.findFirst({ + where: { + AND: [ + entityPrismaWhereClause({ userId: user.id }), + { + id: input.id, + }, + ], + }, + include: { + team: { select: { slug: true, name: true } }, + _count: { + select: { + responses: true, + }, + }, + }, + }); + + if (!form) { + return null; + } + + return await getSerializableForm({ form }); +}; + +export default formQueryHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/formQuery.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/formQuery.schema.ts new file mode 100644 index 00000000000000..704fe596892aeb --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/formQuery.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZFormQueryInputSchema = z.object({ + id: z.string(), +}); + +export type TFormQueryInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/forms.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/forms.handler.ts new file mode 100644 index 00000000000000..0eb5659a3e11c4 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/forms.handler.ts @@ -0,0 +1,153 @@ +import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils"; +import logger from "@calcom/lib/logger"; +import type { PrismaClient } from "@calcom/prisma"; +import type { Prisma } from "@calcom/prisma/client"; +import { entries } from "@calcom/prisma/zod-utils"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TFormSchema } from "./forms.schema"; + +interface FormsHandlerOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TFormSchema; +} +const log = logger.getSubLogger({ prefix: ["[formsHandler]"] }); + +export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => { + const { prisma, user } = ctx; + + const where = getPrismaWhereFromFilters(user, input?.filters); + log.debug("Getting forms where", JSON.stringify(where)); + + const forms = await prisma.app_RoutingForms_Form.findMany({ + where, + orderBy: [ + { + position: "desc", + }, + { + createdAt: "desc", + }, + ], + include: { + team: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + responses: true, + }, + }, + }, + }); + + const totalForms = await prisma.app_RoutingForms_Form.count({ + where: entityPrismaWhereClause({ + userId: user.id, + }), + }); + + const serializableForms = await Promise.all( + forms.map(async (form) => { + const [hasWriteAccess, serializedForm] = await Promise.all([ + canEditEntity(form, user.id), + getSerializableForm({ form }), + ]); + + return { + form: serializedForm, + readOnly: !hasWriteAccess, + }; + }) + ); + + return { + filtered: serializableForms, + totalCount: totalForms, + }; +}; + +export default formsHandler; +type SupportedFilters = Omit["filters"]>, "upIds"> | undefined; + +export function getPrismaWhereFromFilters( + user: { + id: number; + }, + filters: SupportedFilters +) { + const where = { + OR: [] as Prisma.App_RoutingForms_FormWhereInput[], + }; + + const prismaQueries: Record< + keyof NonNullable, + (...args: [number[]]) => Prisma.App_RoutingForms_FormWhereInput + > & { + all: () => Prisma.App_RoutingForms_FormWhereInput; + } = { + userIds: (userIds: number[]) => ({ + userId: { + in: userIds, + }, + teamId: null, + }), + teamIds: (teamIds: number[]) => ({ + team: { + id: { + in: teamIds ?? [], + }, + members: { + some: { + userId: user.id, + accepted: true, + }, + }, + }, + }), + all: () => ({ + OR: [ + { + userId: user.id, + }, + { + team: { + members: { + some: { + userId: user.id, + accepted: true, + }, + }, + }, + }, + ], + }), + }; + + if (!filters || !hasFilter(filters)) { + where.OR.push(prismaQueries.all()); + } else { + for (const entry of entries(filters)) { + if (!entry) { + continue; + } + const [filterName, filter] = entry; + const getPrismaQuery = prismaQueries[filterName]; + // filter might be accidentally set undefined as well + if (!getPrismaQuery || !filter) { + continue; + } + where.OR.push(getPrismaQuery(filter)); + } + } + + return where; +} diff --git a/packages/trpc/server/routers/viewer/routing-forms/forms.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/forms.schema.ts new file mode 100644 index 00000000000000..343097874f3623 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/forms.schema.ts @@ -0,0 +1,13 @@ +"use client"; + +import { z } from "zod"; + +import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; + +export const ZFormsInputSchema = z + .object({ + filters: filterQuerySchemaStrict.optional(), + }) + .nullish(); + +export type TFormSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.handler.ts new file mode 100644 index 00000000000000..0e48feee4b320d --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.handler.ts @@ -0,0 +1,32 @@ +import { MembershipRepository } from "@calcom/lib/server/repository/membership"; +import { getAttributesForTeam } from "@calcom/lib/service/attribute/server/getAttributes"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import { TRPCError } from "@trpc/server"; + +import type { TGetAttributesForTeamInputSchema } from "./getAttributesForTeam.schema"; + +type GetAttributesForTeamHandlerOptions = { + ctx: { + user: NonNullable; + }; + input: TGetAttributesForTeamInputSchema; +}; + +export default async function getAttributesForTeamHandler({ + ctx, + input, +}: GetAttributesForTeamHandlerOptions) { + const { teamId } = input; + const { user } = ctx; + const isMemberOfTeam = await MembershipRepository.findFirstByUserIdAndTeamId({ userId: user.id, teamId }); + + if (!isMemberOfTeam) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "You are not a member of this team", + }); + } + + return getAttributesForTeam({ teamId }); +} diff --git a/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.schema.ts new file mode 100644 index 00000000000000..102d396515d802 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/getAttributesForTeam.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZGetAttributesForTeamInputSchema = z.object({ + teamId: z.number(), +}); + +export type TGetAttributesForTeamInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.handler.ts new file mode 100644 index 00000000000000..2f5ecdf943e9b1 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.handler.ts @@ -0,0 +1,96 @@ +import { enabledIncompleteBookingApps } from "@calcom/features/routing-forms/lib/enabledIncompleteBookingApps"; +import type { PrismaClient } from "@calcom/prisma"; +import { TRPCError } from "@calcom/trpc/server"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TGetIncompleteBookingSettingsInputSchema } from "./getIncompleteBookingSettings.schema"; + +interface GetIncompleteBookingSettingsOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TGetIncompleteBookingSettingsInputSchema; +} + +const getInCompleteBookingSettingsHandler = async (options: GetIncompleteBookingSettingsOptions) => { + const { + ctx: { prisma }, + input, + } = options; + + const [incompleteBookingActions, form] = await Promise.all([ + prisma.app_RoutingForms_IncompleteBookingActions.findMany({ + where: { + formId: input.formId, + }, + }), + prisma.app_RoutingForms_Form.findFirst({ + where: { + id: input.formId, + }, + select: { + userId: true, + teamId: true, + }, + }), + ]); + + if (!form) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Form not found", + }); + } + + const teamId = form?.teamId; + const userId = form.userId; + + if (teamId) { + // Need to get the credentials for the team and org + const orgQuery = await prisma.team.findFirst({ + where: { + id: teamId, + }, + select: { + parentId: true, + }, + }); + + const credentials = await prisma.credential.findMany({ + where: { + appId: { + in: enabledIncompleteBookingApps, + }, + teamId: { + in: [teamId, ...(orgQuery?.parentId ? [orgQuery.parentId] : [])], + }, + }, + include: { + team: { + select: { + name: true, + }, + }, + }, + }); + + return { incompleteBookingActions, credentials }; + } + + if (userId) { + // Assume that a user will have one credential per app + const credential = await prisma.credential.findFirst({ + where: { + appId: { + in: enabledIncompleteBookingApps, + }, + userId, + }, + }); + + return { incompleteBookingActions, credentials: credential ? [{ ...credential, team: null }] : [] }; + } +}; + +export default getInCompleteBookingSettingsHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.schema.ts new file mode 100644 index 00000000000000..8f04950cd22f94 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/getIncompleteBookingSettings.schema.ts @@ -0,0 +1,9 @@ +import z from "zod"; + +export const ZGetIncompleteBookingSettingsInputSchema = z.object({ + formId: z.string(), +}); + +export type TGetIncompleteBookingSettingsInputSchema = z.infer< + typeof ZGetIncompleteBookingSettingsInputSchema +>; diff --git a/packages/trpc/server/routers/viewer/routing-forms/getResponseWithFormFields.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/getResponseWithFormFields.handler.ts new file mode 100644 index 00000000000000..b84fd2da71a2fe --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/getResponseWithFormFields.handler.ts @@ -0,0 +1,97 @@ +import type { z } from "zod"; + +import { enrichFormWithMigrationData } from "@calcom/features/routing-forms/enrichFormWithMigrationData"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import type { FormResponse } from "@calcom/features/routing-forms/types/types"; +import { canAccessEntity } from "@calcom/lib/entityPermissionUtils"; +import { getTranslation } from "@calcom/lib/server/i18n"; +import { prisma } from "@calcom/prisma"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import { TRPCError } from "@trpc/server"; + +import type { ZFormByResponseIdInputSchema } from "./_router"; + +type GetResponseWithFormFieldsOptions = { + ctx: { + user: NonNullable; + }; + input: z.infer; +}; + +async function getResponseWithFormFieldsHandler({ ctx, input }: GetResponseWithFormFieldsOptions) { + const { user } = ctx; + const { formResponseId } = input; + const translate = await getTranslation(user.locale ?? "en", "common"); + + const formResponse = await prisma.app_RoutingForms_FormResponse.findUnique({ + where: { + id: formResponseId, + }, + include: { + form: { + include: { + user: { + select: { + id: true, + movedToProfileId: true, + organization: { + select: { + slug: true, + }, + }, + username: true, + theme: true, + brandColor: true, + darkBrandColor: true, + metadata: true, + }, + }, + team: { + select: { + id: true, + members: true, + slug: true, + parent: { + select: { slug: true }, + }, + parentId: true, + metadata: true, + }, + }, + }, + }, + }, + }); + + if (!formResponse) { + throw new TRPCError({ + code: "NOT_FOUND", + message: translate("form_response_not_found"), + }); + } + + const form = formResponse.form; + + // TODO: To make the check stricter, we could check if the user is admin/owner of the team or a member that is the organizer. + // But the exact criteria of showing a booking to the user could be trickier. Maybe we allow hosts as well to access the booking and thus should allow them as well to reroute + if (!(await canAccessEntity(form, user.id))) { + throw new TRPCError({ + code: "FORBIDDEN", + message: translate("you_dont_have_access_to_reroute_this_booking"), + }); + } + + const { UserRepository } = await import("@calcom/lib/server/repository/user"); + const formWithUserProfile = { + ...form, + user: await UserRepository.enrichUserWithItsProfile({ user: form.user }), + }; + + return { + response: formResponse.response as FormResponse, + form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }), + }; +} + +export default getResponseWithFormFieldsHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/procedures/forms.ts b/packages/trpc/server/routers/viewer/routing-forms/procedures/forms.ts new file mode 100644 index 00000000000000..3c3f47dbcd35df --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/procedures/forms.ts @@ -0,0 +1,13 @@ +import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure"; + +import { importHandler } from "../../../../trpc"; +import { ZFormsInputSchema } from "../forms.schema"; + +const NAMESPACE = "routingForms"; + +const namespaced = (s: string) => `${NAMESPACE}.${s}`; + +export const forms = authedProcedure.input(ZFormsInputSchema).query(async ({ ctx, input }) => { + const handler = await importHandler(namespaced("forms"), () => import("../forms.handler")); + return handler({ ctx, input }); +}); diff --git a/packages/trpc/server/routers/viewer/routing-forms/report.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/report.handler.ts new file mode 100644 index 00000000000000..da7badac752c53 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/report.handler.ts @@ -0,0 +1,184 @@ +import type { z } from "zod"; + +import { jsonLogicToPrisma } from "@calcom/features/routing-forms/jsonLogicToPrisma"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { + ensureStringOrStringArray, + getLabelsFromOptionIds, +} from "@calcom/features/routing-forms/lib/reportingUtils"; +import type { FormResponse } from "@calcom/features/routing-forms/types/types"; +import type { zodFieldView } from "@calcom/features/routing-forms/zod"; +import logger from "@calcom/lib/logger"; +import type { PrismaClient } from "@calcom/prisma"; +import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client"; +import { TRPCError } from "@calcom/trpc/server"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TReportInputSchema } from "./report.schema"; + +interface ReportHandlerOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TReportInputSchema; +} + +const makeFormatDate = (locale: string, timeZone: string) => { + const formatDate = (date: Date): string => { + return new Intl.DateTimeFormat(locale, { + timeZone, + dateStyle: "medium", + timeStyle: "short", + }).format(date); + }; + return formatDate; +}; + +const getRows = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => { + // Can be any prisma `where` clause + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const prismaWhere: Record = input.jsonLogicQuery + ? jsonLogicToPrisma(input.jsonLogicQuery) + : {}; + const skip = input.cursor ?? 0; + const take = input.limit ? input.limit + 1 : 50; + + logger.debug( + `Built Prisma where ${JSON.stringify(prismaWhere)} from jsonLogicQuery ${JSON.stringify( + input.jsonLogicQuery + )}` + ); + const rows = await prisma.app_RoutingForms_FormResponse.findMany({ + where: { + formId: input.formId, + ...prismaWhere, + }, + include: { + routedToBooking: { + select: { + createdAt: true, + user: { + select: { id: true, name: true, email: true }, + }, + assignmentReason: { + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + take, + skip, + }); + return { skip, take, rows }; +}; + +export const reportHandler = async (options: ReportHandlerOptions) => { + const { + ctx: { prisma }, + input, + } = options; + const form = await prisma.app_RoutingForms_Form.findUnique({ + where: { + id: input.formId, + }, + }); + + if (!form) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Form not found", + }); + } + // TODO: Second argument is required to return deleted operators. + const serializedForm = await getSerializableForm({ form, withDeletedFields: true }); + const { rows, skip, take } = await getRows(options); + + const fields = serializedForm?.fields || []; + const { responses, headers } = buildResponsesForReporting({ + responsesFromDb: rows.map((r) => r.response), + fields, + }); + + const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take; + return presenter({ + rows, + options, + headers, + responses, + nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length, + }); +}; + +/** + * This is a temporary solution to make the report work. It should be incorporated into the report data itself. + * Right now we cannot filter by Routed To and Booked At because they are not part of the response data. + */ +function presenter(args: { + rows: Awaited>["rows"]; + options: ReportHandlerOptions; + headers: string[]; + responses: string[][]; + nextCursor: number | null; +}) { + const { headers, responses, nextCursor, options, rows } = args; + const { ctx } = options; + const formatDate = makeFormatDate(ctx.user.locale, ctx.user.timeZone); + return { + nextCursor, + headers: [...headers, "Routed To", "Assignment Reason", "Booked At", "Submitted At"], + responses: responses.map((r, i) => { + const currentRow = rows[i]; + return [ + ...r, + currentRow.routedToBooking?.user?.email || "", + currentRow.routedToBooking?.assignmentReason.length + ? currentRow.routedToBooking.assignmentReason[0].reasonString + : "", + currentRow.routedToBooking?.createdAt ? formatDate(currentRow.routedToBooking.createdAt) : "", + formatDate(currentRow.createdAt), + ]; + }), + }; +} + +export default reportHandler; + +export function buildResponsesForReporting({ + responsesFromDb, + fields, +}: { + responsesFromDb: App_RoutingForms_FormResponse["response"][]; + fields: Pick, "id" | "options" | "label" | "deleted">[]; +}) { + const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : "")); + const responses: string[][] = []; + responsesFromDb.forEach((r) => { + const rowResponses: string[] = []; + responses.push(rowResponses); + fields.forEach((field) => { + if (!r) { + return; + } + const response = r as FormResponse; + const value = response[field.id]?.value || ""; + if (field.options) { + const optionIds = ensureStringOrStringArray(value); + const labels = getLabelsFromOptionIds({ options: field.options, optionIds }); + rowResponses.push(labels.join(", ")); + } else { + const arrayOfValues = value instanceof Array ? value : [value]; + const transformedValue = arrayOfValues.join(", "); + rowResponses.push(transformedValue); + } + }); + }); + + return { responses, headers }; +} diff --git a/packages/trpc/server/routers/viewer/routing-forms/report.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/report.schema.ts new file mode 100644 index 00000000000000..a7f0270d66b922 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/report.schema.ts @@ -0,0 +1,12 @@ +import z from "zod"; + +export const ZReportInputSchema = z.object({ + limit: z.number().default(50), + formId: z.string(), + jsonLogicQuery: z.object({ + logic: z.union([z.record(z.any()), z.null()]), + }), + cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type +}); + +export type TReportInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/routing-forms/response.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/response.handler.ts index d58c347b89c91d..4d4de3bc7f85f7 100644 --- a/packages/trpc/server/routers/viewer/routing-forms/response.handler.ts +++ b/packages/trpc/server/routers/viewer/routing-forms/response.handler.ts @@ -1,5 +1,5 @@ -import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm"; -import { handleResponse } from "@calcom/app-store/routing-forms/lib/handleResponse"; +import { getSerializableForm } from "@calcom/features/routing-forms/lib/getSerializableForm"; +import { handleResponse } from "@calcom/features/routing-forms/lib/handleResponse"; import type { PrismaClient } from "@calcom/prisma"; import { TRPCError } from "@calcom/trpc/server"; diff --git a/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.handler.ts new file mode 100644 index 00000000000000..c61c2f8469f199 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.handler.ts @@ -0,0 +1,78 @@ +import incompleteBookingActionDataSchemas from "@calcom/features/routing-forms/lib/incompleteBooking/actionDataSchemas"; +import logger from "@calcom/lib/logger"; +import type { PrismaClient } from "@calcom/prisma"; +import { TRPCError } from "@calcom/trpc/server"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TSaveIncompleteBookingSettingsInputSchema } from "./saveIncompleteBookingSettings.schema"; + +const log = logger.getSubLogger({ prefix: ["incomplete-booking"] }); + +interface SaveIncompleteBookingSettingsOptions { + ctx: { + prisma: PrismaClient; + user: NonNullable; + }; + input: TSaveIncompleteBookingSettingsInputSchema; +} + +const saveIncompleteBookingSettingsHandler = async (options: SaveIncompleteBookingSettingsOptions) => { + const { + ctx: { prisma }, + input, + } = options; + + const { formId, actionType, data } = input; + + const dataSchema = incompleteBookingActionDataSchemas[actionType]; + + if (!dataSchema) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Action data schema not found", + }); + } + + const parsedData = dataSchema.safeParse(data); + + if (!parsedData.success) { + log.error("Data is not valid", data, parsedData.error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Data is not valid", + }); + } + + // Check to see if the action already exists + const existingAction = await prisma.app_RoutingForms_IncompleteBookingActions.findFirst({ + where: { + formId: formId, + actionType: actionType, + }, + }); + + if (existingAction) { + await prisma.app_RoutingForms_IncompleteBookingActions.update({ + where: { + id: existingAction.id, + }, + data: { + data: parsedData.data, + enabled: input.enabled, + credentialId: input?.credentialId, + }, + }); + } else { + await prisma.app_RoutingForms_IncompleteBookingActions.create({ + data: { + formId: formId, + actionType: actionType, + data: parsedData.data, + enabled: input.enabled, + credentialId: input?.credentialId, + }, + }); + } +}; + +export default saveIncompleteBookingSettingsHandler; diff --git a/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.schema.ts b/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.schema.ts new file mode 100644 index 00000000000000..9fda0a49ca15c7 --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/saveIncompleteBookingSettings.schema.ts @@ -0,0 +1,15 @@ +import z from "zod"; + +import { IncompleteBookingActionType } from "@calcom/prisma/enums"; + +export const ZSaveIncompleteBookingSettingsInputSchema = z.object({ + formId: z.string(), + actionType: z.nativeEnum(IncompleteBookingActionType), + data: z.record(z.any()).optional(), + enabled: z.boolean(), + credentialId: z.number().optional(), +}); + +export type TSaveIncompleteBookingSettingsInputSchema = z.infer< + typeof ZSaveIncompleteBookingSettingsInputSchema +>; diff --git a/packages/trpc/server/routers/viewer/routing-forms/utils.ts b/packages/trpc/server/routers/viewer/routing-forms/utils.ts new file mode 100644 index 00000000000000..7af6e3aaa21a0a --- /dev/null +++ b/packages/trpc/server/routers/viewer/routing-forms/utils.ts @@ -0,0 +1,234 @@ +import type { App_RoutingForms_Form, User } from "@prisma/client"; + +import dayjs from "@calcom/dayjs"; +import type { SerializableField, OrderedResponses } from "@calcom/features/routing-forms/types/types"; +import type { FormResponse, SerializableForm } from "@calcom/features/routing-forms/types/types"; +import type { Tasker } from "@calcom/features/tasker/tasker"; +import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; +import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; +import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; +import logger from "@calcom/lib/logger"; +import { WebhookTriggerEvents } from "@calcom/prisma/client"; +import type { Ensure } from "@calcom/types/utils"; + +let tasker: Tasker; + +if (typeof window === "undefined") { + import("@calcom/features/tasker") + .then((module) => { + tasker = module.default; + }) + .catch((error) => { + console.error("Failed to load tasker:", error); + }); +} + +const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/trpc/utils"] }); + +type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null }; +export type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record< + string, + { + /** + * Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly) + */ + response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[]; + /** + * @deprecated Use `response` instead + */ + value: FormResponse[keyof FormResponse]["value"]; + } +>; + +function isOptionsField(field: Pick) { + return (field.type === "select" || field.type === "multiselect") && field.options; +} + +export function getFieldResponse({ + field, + fieldResponseValue, +}: { + fieldResponseValue: FormResponse[keyof FormResponse]["value"]; + field: Pick; +}) { + if (!isOptionsField(field)) { + return { + value: fieldResponseValue, + response: fieldResponseValue, + }; + } + + if (!field.options) { + return { + value: fieldResponseValue, + response: fieldResponseValue, + }; + } + + const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue]; + + const chosenOptions = valueArray.map((idOrLabel) => { + const foundOptionById = field.options?.find((option) => { + return option.id === idOrLabel; + }); + if (foundOptionById) { + return { + label: foundOptionById.label, + id: foundOptionById.id, + }; + } else { + return { + label: idOrLabel.toString(), + id: null, + }; + } + }); + return { + // value is a legacy prop that is just sending the labels which can change + value: chosenOptions.map((option) => option.label), + // response is new prop that is sending the label along with id(which doesn't change) + response: chosenOptions, + }; +} + +/** + * Not called in preview mode or dry run mode + */ +export async function onFormSubmission( + form: Ensure< + SerializableForm & { user: Pick; userWithEmails?: string[] }, + "fields" + >, + response: FormResponse, + responseId: number, + chosenAction?: { + type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl"; + value: string; + } +) { + const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {}; + + for (const [fieldId, fieldResponse] of Object.entries(response)) { + const field = form.fields.find((f) => f.id === fieldId); + if (!field) { + throw new Error(`Field with id ${fieldId} not found`); + } + // Use the label lowercased as the key to identify a field. + // TODO: We seem to be using label from the response, Can we not use the field.label + const key = + form.fields.find((f) => f.id === fieldId)?.identifier || + (fieldResponse.label as keyof typeof fieldResponsesByIdentifier); + fieldResponsesByIdentifier[key] = getFieldResponse({ + fieldResponseValue: fieldResponse.value, + field, + }); + } + + const { userId, teamId } = getWebhookTargetEntity(form); + + const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId }); + + const subscriberOptionsFormSubmitted = { + userId, + teamId, + orgId, + triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED, + }; + + const subscriberOptionsFormSubmittedNoEvent = { + userId, + teamId, + orgId, + triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT, + }; + + const webhooksFormSubmitted = await getWebhooks(subscriberOptionsFormSubmitted); + const webhooksFormSubmittedNoEvent = await getWebhooks(subscriberOptionsFormSubmittedNoEvent); + + const promisesFormSubmitted = webhooksFormSubmitted.map((webhook) => { + sendGenericWebhookPayload({ + secretKey: webhook.secret, + triggerEvent: "FORM_SUBMITTED", + createdAt: new Date().toISOString(), + webhook, + data: { + formId: form.id, + formName: form.name, + teamId: form.teamId, + responses: fieldResponsesByIdentifier, + }, + rootData: { + // Send responses unwrapped at root level for backwards compatibility + ...Object.entries(fieldResponsesByIdentifier).reduce((acc, [key, value]) => { + acc[key] = value.value; + return acc; + }, {} as Record), + }, + }).catch((e) => { + console.error(`Error executing routing form webhook`, webhook, e); + }); + }); + + const promisesFormSubmittedNoEvent = webhooksFormSubmittedNoEvent.map((webhook) => { + const scheduledAt = dayjs().add(15, "minute").toDate(); + + return tasker.create( + "triggerFormSubmittedNoEventWebhook", + { + responseId, + form, + responses: fieldResponsesByIdentifier, + redirect: chosenAction, + webhook, + }, + { scheduledAt } + ); + }); + + const promises = [...promisesFormSubmitted, ...promisesFormSubmittedNoEvent]; + + await Promise.all(promises); + const orderedResponses = form.fields.reduce((acc, field) => { + acc.push(response[field.id]); + return acc; + }, [] as OrderedResponses); + + if (form.teamId) { + if (form.userWithEmails?.length) { + moduleLogger.debug( + `Preparing to send Form Response email for Form:${form.id} to users: ${form.userWithEmails.join(",")}` + ); + await sendResponseEmail(form, orderedResponses, form.userWithEmails); + } + } else if (form.settings?.emailOwnerOnSubmission) { + moduleLogger.debug( + `Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}` + ); + await sendResponseEmail(form, orderedResponses, [form.user.email]); + } +} + +export const sendResponseEmail = async ( + form: Pick, + orderedResponses: OrderedResponses, + toAddresses: string[] +) => { + try { + if (typeof window === "undefined") { + const { default: ResponseEmail } = await import( + "@calcom/features/routing-forms/emails/templates/response-email" + ); + const email = new ResponseEmail({ form: form, toAddresses, orderedResponses }); + await email.sendEmail(); + } + } catch (e) { + moduleLogger.error("Error sending response email", e); + } +}; + +function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) { + // If it's a team form, the target must be team webhook + // If it's a user form, the target must be user webhook + const isTeamForm = form.teamId; + return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null }; +} diff --git a/routing-forms-migration-status.md b/routing-forms-migration-status.md new file mode 100644 index 00000000000000..dd43996d55ce0f --- /dev/null +++ b/routing-forms-migration-status.md @@ -0,0 +1,37 @@ +# Routing Forms Migration Status + +## Overview +- We are migrating Routing Forms from packages/app-store to @calcom/features/routing-forms +- Pages exist in apps/web/pages/routing-forms +- Migration is in progress + +## Rules +- No file must be imported from @calcom/app-store/routing-forms. All files must be moved to their respective places outside app-store +- We don't want package.json in features/routing-forms +- Other apps like salesforce must be imported from the old place which is @calcom/app-store. Only Routing Form is migrated +- All [..appPages].tsx files should be renamed to index.tsx +- prisma, user, ssrInit aren't valid getServerSideProps parameters. Use the supported parameters and import the everything else. Refer to existing getServerSideProps functions for reference +- We need enrichFormWithMigrationData where it is. Don't remove it. +- We do't need appUrl now as it is not an app anymore. We can use reguarl WEBAPP_URL +- @calcom/routing-forms -> becomes -> @calcom/features/routing-forms +- @calcom/features/routing-forms/lib must be used instead of @calcom/web/lib/routing-forms. We can delete the apps/web/lib/routing-forms directory if all files are there in @calcom/features/routing-forms/lib +- Relative imports like ../../components/SingleForm should be updated to @calcom/features/routing-forms/components/SingleForm in all page routes in apps/web/pages/routing-forms +- Repositories are class with static methods. +- There must not be features/routing-form/pages directory. All pages should be in apps/web/pages/routing-forms +- Ask for error logs of server or TS logs if request fails to verify + +🚧 In Progress: +- Getting verification script to pass + +❌ Issues Fixed: +- Import error in appDataSchemas.ts trying to import salesforce from wrong location +- Verification script failing for /routing-forms/form-edit endpoint due to incorrect getServerSideProps setup + +## Next Steps +1. routing-link route is failing +1. Get all verification script endpoints passing +2. Remove the old routing-forms directory from app-store once verified + +## Verification +- We have a verification script node scripts/verify-routing-forms-migration.js that should have passing endpoints +- Current status: ❌ Not passing \ No newline at end of file diff --git a/scripts/migrate-routing-forms.js b/scripts/migrate-routing-forms.js new file mode 100644 index 00000000000000..ae8986f1a157b4 --- /dev/null +++ b/scripts/migrate-routing-forms.js @@ -0,0 +1,94 @@ +const fs = require("fs-extra"); +const path = require("path"); + +const SOURCE_DIR = "packages/app-store/routing-forms"; +const DEST_DIR = "packages/features/routing-forms"; + +// Directories to migrate +const DIRS_TO_MIGRATE = [ + "pages", + "lib", + "types", + "trpc", + "playwright", + "static", + "components", + "emails", + "__tests__", + "api", +]; + +// Files to migrate +const FILES_TO_MIGRATE = [ + "zod.ts", + "trpc-router.ts", + "appBookingFormHandler.ts", + "appComponents.ts", + "appDataSchemas.ts", + "config.json", + "enrichFormWithMigrationData.ts", + "env.d.ts", + "getEventTypeRedirectUrl.ts", + "index.ts", + "jsonLogicToPrisma.ts", +]; + +async function createDirectoryStructure() { + console.log("Creating directory structure..."); + for (const dir of DIRS_TO_MIGRATE) { + const destPath = path.join(DEST_DIR, dir); + await fs.ensureDir(destPath); + console.log(`Created directory: ${destPath}`); + } +} + +async function copyFiles() { + console.log("\nCopying files..."); + for (const file of FILES_TO_MIGRATE) { + const sourcePath = path.join(SOURCE_DIR, file); + const destPath = path.join(DEST_DIR, file); + try { + await fs.copy(sourcePath, destPath); + console.log(`Copied: ${file}`); + } catch (error) { + console.error(`Failed to copy ${file}:`, error.message); + } + } +} + +async function copyDirectories() { + console.log("\nCopying directories..."); + for (const dir of DIRS_TO_MIGRATE) { + const sourcePath = path.join(SOURCE_DIR, dir); + const destPath = path.join(DEST_DIR, dir); + try { + await fs.copy(sourcePath, destPath); + console.log(`Copied directory: ${dir}`); + } catch (error) { + console.error(`Failed to copy directory ${dir}:`, error.message); + } + } +} + +async function main() { + console.log("Starting Routing Forms migration..."); + + try { + await createDirectoryStructure(); + await copyFiles(); + await copyDirectories(); + + console.log("\nMigration completed successfully!"); + console.log("\nNext steps:"); + console.log( + "1. Update imports in all files to use @calcom/features/routing-forms instead of @calcom/app-store/routing-forms" + ); + console.log("2. Run tests to ensure everything works correctly"); + console.log("3. Remove the old routing-forms directory from app-store once verified"); + } catch (error) { + console.error("Migration failed:", error); + process.exit(1); + } +} + +main(); diff --git a/scripts/update-routing-forms-imports.js b/scripts/update-routing-forms-imports.js new file mode 100644 index 00000000000000..436b05cf5d5b66 --- /dev/null +++ b/scripts/update-routing-forms-imports.js @@ -0,0 +1,104 @@ +const fs = require("fs-extra"); +const path = require("path"); +const glob = require("glob"); + +const OLD_IMPORT = "@calcom/app-store/routing-forms"; +const NEW_IMPORT = "@calcom/features/routing-forms"; +const OLD_TYPES_IMPORT = "@/types/routing-forms"; + +// Directories to exclude +const EXCLUDE_DIRS = ["node_modules", "dist", ".next", "build", "packages/app-store/routing-forms"]; + +// File extensions to process +const FILE_EXTENSIONS = "{ts,tsx,js,jsx}"; + +function updateImports(filePath) { + const content = fs.readFileSync(filePath, "utf8"); + + // Skip if file doesn't contain any of the old imports + if ( + !content.includes(OLD_IMPORT) && + !content.includes(OLD_TYPES_IMPORT) && + !content.includes("@/routing-forms") + ) { + return false; + } + + // Replace imports + let updatedContent = content + // Replace @calcom/app-store/routing-forms imports + .replace(new RegExp(`from ["']${OLD_IMPORT}(/[^"']*)?["']`, "g"), (match) => + match.replace(OLD_IMPORT, NEW_IMPORT) + ) + .replace(new RegExp(`import\\(["']${OLD_IMPORT}(/[^"']*)?["']\\)`, "g"), (match) => + match.replace(OLD_IMPORT, NEW_IMPORT) + ) + .replace(new RegExp(`require\\(["']${OLD_IMPORT}(/[^"']*)?["']\\)`, "g"), (match) => + match.replace(OLD_IMPORT, NEW_IMPORT) + ) + // Replace @/types/routing-forms imports + .replace(new RegExp(`from ["']${OLD_TYPES_IMPORT}(/[^"']*)?["']`, "g"), (match) => + match.replace(OLD_TYPES_IMPORT, `${NEW_IMPORT}/types`) + ) + // Replace any other @/routing-forms imports + .replace(new RegExp(`from ["']@/routing-forms(/[^"']*)?["']`, "g"), (match) => + match.replace("@/routing-forms", NEW_IMPORT) + ); + + if (content !== updatedContent) { + fs.writeFileSync(filePath, updatedContent, "utf8"); + console.log(`Updated imports in: ${filePath}`); + return true; + } + return false; +} + +function findFiles() { + const excludePattern = EXCLUDE_DIRS.map((dir) => `**/${dir}/**`); + const files = []; + + // Search in common directories + ["apps", "packages"].forEach((baseDir) => { + const pattern = `${baseDir}/**/*.${FILE_EXTENSIONS}`; + const matches = glob.sync(pattern, { + ignore: excludePattern, + nodir: true, + }); + files.push(...matches); + }); + + return files; +} + +async function main() { + console.log("Starting import updates..."); + + try { + const files = findFiles(); + let updatedCount = 0; + + console.log(`Found ${files.length} files to check...`); + + for (const file of files) { + if (updateImports(file)) { + updatedCount++; + } + } + + console.log(`\nCompleted successfully!`); + console.log(`Updated ${updatedCount} files`); + + if (updatedCount > 0) { + console.log("\nNext steps:"); + console.log("1. Run tests to verify the changes"); + console.log("2. Check git diff to ensure changes are correct"); + } else { + console.log("\nNo files needed updating"); + } + } catch (error) { + console.error("Failed to update imports:", error); + process.exit(1); + } +} + +main(); diff --git a/scripts/verify-routing-forms-migration.js b/scripts/verify-routing-forms-migration.js new file mode 100644 index 00000000000000..0626f84f0425e1 --- /dev/null +++ b/scripts/verify-routing-forms-migration.js @@ -0,0 +1,79 @@ +const fetch = require("node-fetch"); +const assert = require("assert"); + +const BASE_URL = "http://localhost:3000"; +const ROUTES = ["/routing-forms/form-edit", "/routing-forms/forms", "/routing-forms/route-builder"]; + +const ROUTES_WITH_404 = ["/routing-forms/routing-link/test-form-id"]; + +// TRPC endpoint to test +const TRPC_ENDPOINT = "/api/trpc/viewer.routingForms.forms"; + +// Add delay between requests +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function verifyRoute(route, expectedStatus = 200) { + try { + console.log(`Testing route ${route}...`); + const response = await fetch(`${BASE_URL}${route}`); + console.log(`Route ${route} returned status code: ${response.status}`); + assert.strictEqual(response.status, expectedStatus, `Route ${route} should return ${expectedStatus}`); + const html = await response.text(); + assert.ok(html.includes(""), `Route ${route} should return HTML`); + console.log(`✅ Route ${route} is working`); + // Add 2 second delay between requests + await delay(2000); + } catch (error) { + console.error(`❌ Route ${route} failed:`, error.message); + process.exit(1); + } +} + +async function verifyTRPC() { + try { + console.log("Testing TRPC endpoint..."); + const response = await fetch(`${BASE_URL}${TRPC_ENDPOINT}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + json: { + type: "query", + }, + }), + }); + + assert.ok( + [200, 401].includes(response.status), + `TRPC endpoint should return 200 or 401 (if unauthorized)` + ); + console.log("✅ TRPC endpoint is responding"); + } catch (error) { + console.error("❌ TRPC endpoint failed:", error.message); + process.exit(1); + } +} + +async function main() { + console.log("Starting verification..."); + console.log("Waiting 5 seconds for dev server to be ready..."); + await delay(5000); + + // Verify all routes + for (const route of ROUTES) { + await verifyRoute(route); + } + + // Verify routes that should return 404 + for (const route of ROUTES_WITH_404) { + await verifyRoute(route, 404); + } + + // Verify TRPC endpoint + await verifyTRPC(); + + console.log("All verifications passed! ✨"); +} + +main().catch(console.error); diff --git a/yarn.lock b/yarn.lock index 1af8f050d558f7..bac35b7456c66c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4656,19 +4656,6 @@ __metadata: languageName: unknown linkType: soft -"@calcom/routing-forms@workspace:packages/app-store/routing-forms": - version: 0.0.0-use.local - resolution: "@calcom/routing-forms@workspace:packages/app-store/routing-forms" - dependencies: - "@calcom/lib": "*" - "@calcom/types": "*" - "@types/json-logic-js": ^1.2.1 - dotenv: ^16.3.1 - json-logic-js: ^2.0.2 - react-awesome-query-builder: ^5.1.2 - languageName: unknown - linkType: soft - "@calcom/salesforce@workspace:packages/app-store/salesforce": version: 0.0.0-use.local resolution: "@calcom/salesforce@workspace:packages/app-store/salesforce"