diff --git a/packages/app-store/routing-forms/components/FormActions.tsx b/packages/app-store/routing-forms/components/FormActions.tsx index b1a9740824..1e04b9cef8 100644 --- a/packages/app-store/routing-forms/components/FormActions.tsx +++ b/packages/app-store/routing-forms/components/FormActions.tsx @@ -1,9 +1,7 @@ -import type { App_RoutingForms_Form } from "@prisma/client"; import { useRouter } from "next/navigation"; import { createContext, forwardRef, useContext, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; -import { z } from "zod"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/features/embed/RoutingFormEmbed"; @@ -34,15 +32,24 @@ import { } from "@calcom/ui"; import getFieldIdentifier from "../lib/getFieldIdentifier"; -import type { SerializableForm } from "../types/types"; -type RoutingForm = SerializableForm; +type FormField = { + identifier?: string; + id: string; + type: string; + label: string; + routerId?: string | null; +}; + +type RoutingForm = { + id: string; + name: string; + disabled: boolean; + fields?: FormField[]; +}; + export type NewFormDialogState = { action: "new" | "duplicate"; target: string | null } | null; export type SetNewFormDialogState = React.Dispatch>; -const newFormModalQuerySchema = z.object({ - action: z.literal("new").or(z.literal("duplicate")), - target: z.string().optional(), -}); function NewFormDialog({ appUrl, diff --git a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx index 58b69ef068..4ed6c57c23 100644 --- a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx @@ -201,7 +201,10 @@ export default function RoutingForms({ SkeletonLoader={SkeletonLoaderTeamList}>
- {forms?.map(({ form, readOnly }, index) => { + {forms?.map(({ form, readOnly, hasError }, index) => { + // Make the form read only if it has an error + // TODO: Consider showing error in UI so user can report and get it fixed. + readOnly = readOnly || hasError; if (!form) { return null; } diff --git a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx b/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx index 8827a57ea2..0f84edc056 100644 --- a/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx @@ -8,11 +8,11 @@ import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder"; import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-builder"; import type { JsonTree } from "react-awesome-query-builder"; import type { UseFormReturn } from "react-hook-form"; +import type { z } from "zod"; import { areTheySiblingEntitites } from "@calcom/lib/entityPermissionUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { buildEmptyQueryValue, raqbQueryValueUtils } 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"; @@ -49,7 +49,6 @@ import { isDynamicOperandField, } from "../../lib/getQueryBuilderConfig"; import isRouter from "../../lib/isRouter"; -import type { SerializableForm } from "../../types/types"; import type { GlobalRoute, LocalRoute, @@ -58,6 +57,7 @@ import type { EditFormRoute, AttributeRoutingConfig, } from "../../types/types"; +import type { zodRoutes } from "../../zod"; import { RouteActionType } from "../../zod"; type EventTypesByGroup = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]; @@ -912,7 +912,7 @@ const Routes = ({ const { data: allForms } = trpc.viewer.appRoutingForms.forms.useQuery(); - const notHaveAttributesQuery = ({ form }: { form: SerializableForm }) => { + const notHaveAttributesQuery = ({ form }: { form: { routes: z.infer } }) => { return form.routes?.every((route) => { if (isRouter(route)) { return true; diff --git a/packages/app-store/routing-forms/trpc/forms.handler.ts b/packages/app-store/routing-forms/trpc/forms.handler.ts index 907f3c5a8b..e7b64882c4 100644 --- a/packages/app-store/routing-forms/trpc/forms.handler.ts +++ b/packages/app-store/routing-forms/trpc/forms.handler.ts @@ -1,12 +1,16 @@ +import type { z } from "zod"; + import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils"; import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; 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 { getSerializableForm } from "../lib/getSerializableForm"; +import type { zodFields, zodRoutes } from "../zod"; import type { TFormSchema } from "./forms.schema"; interface FormsHandlerOptions { @@ -55,24 +59,54 @@ export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => { }), }); - 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, + filtered: await buildFormsWithReadOnlyStatus(), totalCount: totalForms, }; + + async function buildFormsWithReadOnlyStatus() { + // Avoid crash of one form to crash entire listing + const settledFormsWithReadonlyStatus = await Promise.allSettled( + forms.map(async (form) => { + const [hasWriteAccess, serializedForm] = await Promise.all([ + canEditEntity(form, user.id), + getSerializableForm({ form }), + ]); + + return { + form: serializedForm, + readOnly: !hasWriteAccess, + }; + }) + ); + + const formsWithReadonlyStatus = settledFormsWithReadonlyStatus.map((result, index) => { + if (result.status === "fulfilled") { + // Normal case + return { + ...result.value, + hasError: false, + }; + } + + // Error case + const form = forms[index]; + log.error(`Error getting form ${form.id}: ${safeStringify(result.reason)}`); + + return { + form: { + ...form, + // Usually the error is in parsing routes/fields as they are JSON. So, we just set them empty, so that form can be still listed. + routes: [] as z.infer, + fields: [] as z.infer, + }, + // Consider it readonly as we don't know the status due to error + readOnly: true, + hasError: true, + }; + }); + return formsWithReadonlyStatus; + } }; export default formsHandler;