fix: Dont crash entire listing if one form has faulty JSON fields (#19368)

* Dont hit members matching endpoint for regular team forms

* fix tests

* Dont crash and instead show the form in readonly mode
This commit is contained in:
Hariom Balhara
2025-02-19 11:05:29 +00:00
committed by GitHub
parent bf9e9b4ae1
commit e87168322c
4 changed files with 71 additions and 27 deletions
@@ -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<App_RoutingForms_Form>;
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<React.SetStateAction<NewFormDialogState>>;
const newFormModalQuerySchema = z.object({
action: z.literal("new").or(z.literal("duplicate")),
target: z.string().optional(),
});
function NewFormDialog({
appUrl,
@@ -201,7 +201,10 @@ export default function RoutingForms({
SkeletonLoader={SkeletonLoaderTeamList}>
<div className="bg-default mb-16 overflow-hidden">
<List data-testid="routing-forms-list" ref={parent}>
{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;
}
@@ -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<App_RoutingForms_Form> }) => {
const notHaveAttributesQuery = ({ form }: { form: { routes: z.infer<typeof zodRoutes> } }) => {
return form.routes?.every((route) => {
if (isRouter(route)) {
return true;
@@ -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<typeof zodRoutes>,
fields: [] as z.infer<typeof zodFields>,
},
// Consider it readonly as we don't know the status due to error
readOnly: true,
hasError: true,
};
});
return formsWithReadonlyStatus;
}
};
export default formsHandler;