From 580165b33b0d0690909323780e6beb742928dfdc Mon Sep 17 00:00:00 2001 From: Lenny Date: Sun, 28 Jun 2026 16:27:46 +0000 Subject: [PATCH] fix forms character counts to ignore whitespace --- .../forms/question-response-card.tsx | 7 +++-- packages/api/src/routers/forms.ts | 12 +++++++-- packages/utils/src/forms.ts | 26 ++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/blade/src/app/_components/forms/question-response-card.tsx b/apps/blade/src/app/_components/forms/question-response-card.tsx index 5d2a9bd9a..9caee613c 100644 --- a/apps/blade/src/app/_components/forms/question-response-card.tsx +++ b/apps/blade/src/app/_components/forms/question-response-card.tsx @@ -31,6 +31,9 @@ import { api } from "~/trpc/react"; type FormQuestion = z.infer; +const countNonWhitespaceCharacters = (value: string) => + value.replace(/\s/g, "").length; + interface QuestionResponseCardProps { question: FormQuestion; value?: string | string[] | number | Date | boolean | null; @@ -111,7 +114,7 @@ function QuestionBody({ case "SHORT_ANSWER": { const currentValue = (value as string) || ""; const maxLength = 150; - const charCount = currentValue.length; + const charCount = countNonWhitespaceCharacters(currentValue); const isOverLimit = charCount > maxLength; return ( @@ -138,7 +141,7 @@ function QuestionBody({ case "PARAGRAPH": { const currentValue = (value as string) || ""; const maxLength = 750; - const charCount = currentValue.length; + const charCount = countNonWhitespaceCharacters(currentValue); const isOverLimit = charCount > maxLength; return ( diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 9e4a46ee8..96370b70f 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1,5 +1,4 @@ import type { TRPCRouterRecord } from "@trpc/server"; -import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; import jsonSchemaToZod from "json-schema-to-zod"; @@ -154,6 +153,15 @@ export const formsRouter = { minioClient, ); + const jsonSchema = forms.generateJsonSchema(formData); + + if (!jsonSchema.success) { + throw new TRPCError({ + message: jsonSchema.msg, + code: "BAD_REQUEST", + }); + } + return { ...retForm, responseRoleIds: responseRoles.map((r) => r.roleId), @@ -161,7 +169,7 @@ export const formsRouter = { ...formData, instructions: instructionsWithFreshUrls, }, - zodValidator: jsonSchemaToZod(form.formValidatorJson as JSONSchema7), + zodValidator: jsonSchemaToZod(jsonSchema.schema), }; }), diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index 68e67dd5d..ba05425b2 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -13,6 +13,15 @@ type OptionalSchema = | { success: true; schema: JSONSchema7 } | { success: false; msg: string }; +function createNonWhitespaceLengthPattern(min?: number, max?: number) { + if (min === undefined && max === undefined) return undefined; + + const lowerBound = min ?? 0; + const upperBound = max ?? ""; + + return `^(?:\\s*\\S){${lowerBound},${upperBound}}\\s*$`; +} + function createJsonSchemaValidator({ optional, type, @@ -30,12 +39,16 @@ function createJsonSchemaValidator({ switch (type) { case "SHORT_ANSWER": - case "PARAGRAPH": + case "PARAGRAPH": { schema.type = "string"; - if (max === undefined) { - schema.maxLength = type === "SHORT_ANSWER" ? 150 : 750; - } + const maxNonWhitespaceLength = + max ?? (type === "SHORT_ANSWER" ? 150 : 750); + schema.pattern = createNonWhitespaceLengthPattern( + min, + maxNonWhitespaceLength, + ); break; + } case "EMAIL": schema.type = "string"; schema.format = "email"; @@ -93,7 +106,8 @@ function createJsonSchemaValidator({ } if (min !== undefined) { - if (schema.type === "string") schema.minLength = min; + if (schema.type === "string" && schema.pattern === undefined) + schema.minLength = min; if (schema.type === "array") schema.minItems = min; if (schema.type === "number") schema.minimum = min; } else { @@ -101,7 +115,7 @@ function createJsonSchemaValidator({ } if (max !== undefined) { - if (schema.type === "string") { + if (schema.type === "string" && schema.pattern === undefined) { // Explicit max value overrides any defaults schema.maxLength = max; }