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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user