From ebf8b202e438502cd434b6b3c6026f5535f50e26 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 00:24:26 -0500 Subject: [PATCH 1/8] fix getUserResponse bad query --- packages/api/src/routers/forms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index beed367f..26303596 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -582,7 +582,7 @@ export const formsRouter = { .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) .where( - and(eq(FormResponse.userId, userId), eq(FormsSchemas.name, form)), + and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form)), ) .orderBy(desc(FormResponse.createdAt)); }), From a29970430478e1f5c0350c1d674db364391f39b6 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 11:06:16 -0500 Subject: [PATCH 2/8] db changes --- packages/db/src/schemas/knight-hacks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts index e6c46289..212e5a54 100644 --- a/packages/db/src/schemas/knight-hacks.ts +++ b/packages/db/src/schemas/knight-hacks.ts @@ -570,6 +570,7 @@ export const FormsSchemas = createTable("form_schemas", (t) => ({ createdAt: t.timestamp().notNull().defaultNow(), duesOnly: t.boolean().notNull().default(false), allowResubmission: t.boolean().notNull().default(false), + allowEdit: t.boolean().notNull().default(false), formData: t.jsonb().notNull(), formValidatorJson: t.jsonb().notNull(), section: t.varchar({ length: 255 }).notNull().default("General"), @@ -610,6 +611,7 @@ export const FormResponse = createTable("form_response", (t) => ({ .references(() => User.id, { onDelete: "cascade" }), responseData: t.jsonb().notNull(), createdAt: t.timestamp().notNull().defaultNow(), + editedAt: t.timestamp().notNull().defaultNow(), })); export const InsertFormResponseSchema = createInsertSchema(FormResponse); From 59de7b49e1595ef2893880f5061f16f6c8588363 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 17:45:57 -0500 Subject: [PATCH 3/8] added bool values for bool question type --- .../[formName]/_components/question-response-card.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx index 631dc891..929be1a5 100644 --- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx @@ -34,7 +34,7 @@ type FormQuestion = z.infer; interface QuestionResponseCardProps { question: FormQuestion; - value?: string | string[] | number | Date | null; + value?: string | string[] | number | Date | Boolean| null; onChange: (value: string | string[] | number | Date | null) => void; onBlur?: () => void; disabled?: boolean; @@ -102,7 +102,7 @@ function QuestionBody({ formId, }: { question: FormQuestion; - value?: string | string[] | number | Date | null; + value?: string | string[] | number | Date | Boolean | null; onChange: (value: string | string[] | number | Date | null) => void; onBlur?: () => void; disabled?: boolean; @@ -753,6 +753,11 @@ function FileUploadInput({ const getUploadUrlMutation = api.forms.getUploadUrl.useMutation(); + // used to sync with responseData for view/edit, otherwise value will be null + React.useEffect(() => { + setFileName(value ? value.split("/").pop() ?? null : null); + }, [value]); + const handleFileUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; From 94f176ce31f842a301595e8a2f661d3310d73667 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 17:46:42 -0500 Subject: [PATCH 4/8] added edit toggle to form creation --- .../src/app/admin/forms/[slug]/client.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 502c301e..07c75528 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -188,6 +188,7 @@ export function EditorClient({ const [instructions, setInstructions] = useState([]); const [duesOnly, setDuesOnly] = useState(false); const [allowResubmission, setAllowResubmission] = useState(true); + const [allowEdit, setAllowEdit] = useState(true); const [responseRoleIds, setResponseRoleIds] = useState([]); const [responseRolesDialogOpen, setResponseRolesDialogOpen] = useState(false); const [activeItemId, setActiveItemId] = useState(null); @@ -257,6 +258,7 @@ export function EditorClient({ }, duesOnly, allowResubmission, + allowEdit, responseRoleIds, } as any); }, [ @@ -267,6 +269,7 @@ export function EditorClient({ instructions, duesOnly, allowResubmission, + allowEdit, formData, isLoading, isFetching, @@ -296,6 +299,7 @@ export function EditorClient({ setFormBanner(formData.formData.banner || ""); setDuesOnly(formData.duesOnly); setAllowResubmission(formData.allowResubmission); + setAllowEdit(formData.allowEdit); setResponseRoleIds((formData as any).responseRoleIds || []); const loadedQuestions: UIQuestion[] = formData.formData.questions.map( @@ -573,12 +577,25 @@ export function EditorClient({ Allow Multiple Responses +
+ + +
- + ))} @@ -53,18 +51,4 @@ export async function FormResponses() { )} ); -} - -function ViewFormResponseButton({ - responseIdSlug, - formNameSlug, -}: { - responseIdSlug: string; - formNameSlug: string; -}) { - return ( - - - - ); -} +} \ No newline at end of file diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 3ed74fa1..b4b370cc 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -3,28 +3,37 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; +import { z } from "zod"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; +import { InstructionResponseCard } from "~/app/forms/[formName]/_components/instruction-response-card"; import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card"; import { api } from "~/trpc/react"; +const emailSchema = z.string().email("Invalid email address"); +const phoneSchema = z.string().regex(/^\+?\d{7,15}$/, "Invalid phone number"); +const linkSchema = z.string().url("Please enter a valid URL"); + interface FormReviewClientProps { formName: string; userName: string; - responseId?: string; + handleCallbacks: (response: Record) => void; + responseId: string; } export function FormReviewClient({ formName, userName, + handleCallbacks, responseId, }: FormReviewClientProps) { const router = useRouter(); const [responses, setResponses] = useState< - Record + Record >({}); + const [touchedFields, setTouchedFields] = useState>(new Set()); const [isSubmitted, setIsSubmitted] = useState(false); const [showCheckmark, setShowCheckmark] = useState(false); const [showText, setShowText] = useState(false); @@ -36,29 +45,42 @@ export function FormReviewClient({ }); // use responseId to query singular response to view - const responseQuery = api.forms.getUserResponse.useQuery({ - responseId, - }); + const responseQuery = api.forms.getUserResponse.useQuery( + { + responseId, + }, + // ensure edited responses are not stale + { + enabled: !!responseId, + staleTime: 0, + }, + ); - // TODO: WILL USE FOR EDIT - const submitResponse = api.forms.createResponse.useMutation({ + const editResponse = api.forms.editResponse.useMutation({ onSuccess: () => { setSubmitError(null); setIsSubmitted(true); }, onError: (error) => { setSubmitError( - error.message || "Failed to submit response. Please try again.", + error.message || "Failed to submit response edit. Please try again.", ); }, }); - useEffect(() => { - const data = responseQuery.data ? responseQuery.data[0] : null; - if (!data?.responseData) return; + if (!responseQuery.data) return; + + const data = responseQuery.data[0]; - const hydrated: Record = - {}; + if (!data?.responseData) { + setResponses({}); + return; + } + + const hydrated: Record< + string, + string | string[] | number | Date | boolean | null + > = {}; for (const [questionText, raw] of Object.entries(data.responseData)) { if (raw === null) hydrated[questionText] = null; @@ -71,15 +93,12 @@ export function FormReviewClient({ } else if (typeof raw === "number") hydrated[questionText] = raw; else if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) hydrated[questionText] = raw; + else if (typeof raw === "boolean") hydrated[questionText] = raw; else hydrated[questionText] = null; } setResponses(hydrated); - }, [responseId, responseQuery.data]); - - useEffect(() => { - setResponses({}); - }, [responseId]); + }, [responseQuery.data]); // Staggered animation for success screen useEffect(() => { @@ -106,11 +125,7 @@ export function FormReviewClient({ }, [isSubmitted, router]); // wait for all queries to load - if ( - formQuery.isLoading || - responseQuery.isLoading || - Object.keys(responses).length === 0 - ) + if (formQuery.isLoading || responseQuery.isLoading || responses === null) return (
@@ -128,10 +143,7 @@ export function FormReviewClient({ const form = formQuery.data.formData; - // TODO: Implement editing - const allowEdit = false; - - const formDisabled = !allowEdit; + const allowEdit = formQuery.data.allowEdit; // SUCESSSSS if (isSubmitted) { @@ -169,8 +181,10 @@ export function FormReviewClient({ })); }; - // TODO: WILL USE FOR EDIT - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleFieldBlur = (questionText: string) => { + setTouchedFields((prev) => new Set(prev).add(questionText)); + }; + const handleSubmit = () => { // Build response data object const responseData: Record = {}; @@ -195,44 +209,142 @@ export function FormReviewClient({ .slice(0, 5); } } else { - responseData[question.question] = response; + // Convert boolean strings to actual booleans for BOOLEAN question type + if (question.type === "BOOLEAN" && typeof response === "string") { + responseData[question.question] = response === "true"; + } else { + responseData[question.question] = response; + } } } }); - submitResponse.mutate({ - form: formQuery.data.id, + editResponse.mutate({ + id: responseId, responseData, }); }; - // TODO: WILL USE FOR EDIT - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const getValidationError = (question: (typeof form.questions)[number]) => { + if (!touchedFields.has(question.question)) { + return null; + } + + const response = responses[question.question]; + + if (question.optional) { + if ( + !response || + response === "" || + (Array.isArray(response) && response.length === 0) + ) { + return null; + } + } else { + if (response === null || response === undefined || response === "") { + return "This field is required."; + } + if (Array.isArray(response) && response.length === 0) { + return "This field is required."; + } + // For required BOOLEAN questions, must be checked (true) + if (question.type === "BOOLEAN") { + const isChecked = + (typeof response === "string" && response === "true") || + (typeof response === "boolean" && response === true); + if (!isChecked) { + return "You must accept this to continue."; + } + } + } + + if (question.type === "EMAIL" && typeof response === "string") { + const result = emailSchema.safeParse(response); + if (!result.success) { + return "Please enter a valid email address"; + } + } + if (question.type === "PHONE" && typeof response === "string") { + const result = phoneSchema.safeParse(response); + if (!result.success) { + return "Please enter a valid phone number (7-15 digits, optional + prefix)"; + } + } + if (question.type === "LINK" && typeof response === "string") { + const result = linkSchema.safeParse(response); + if (!result.success) { + return "Please enter a valid URL"; + } + } + + return null; + }; + const isFormValid = () => { // Check if all required questions have responses return form.questions.every((question) => { - if (question.optional) return true; // Optional questions don't need validation + if (question.optional) { + const response = responses[question.question]; + if ( + !response || + response === "" || + (Array.isArray(response) && response.length === 0) + ) { + return true; + } + + if (question.type === "EMAIL" && typeof response === "string") { + return emailSchema.safeParse(response).success; + } + if (question.type === "PHONE" && typeof response === "string") { + return phoneSchema.safeParse(response).success; + } + if (question.type === "LINK" && typeof response === "string") { + return linkSchema.safeParse(response).success; + } + return true; + } const response = responses[question.question]; if (response === null || response === undefined || response === "") return false; if (Array.isArray(response) && response.length === 0) return false; + + // For required BOOLEAN questions, must be checked (true), not false + if (question.type === "BOOLEAN") { + if (typeof response === "string") { + return response === "true"; // Must be "true" string + } + if (typeof response === "boolean") { + return response === true; // Must be true boolean + } + return false; // Missing or invalid + } + + if (question.type === "EMAIL" && typeof response === "string") { + return emailSchema.safeParse(response).success; + } + if (question.type === "PHONE" && typeof response === "string") { + return phoneSchema.safeParse(response).success; + } + if (question.type === "LINK" && typeof response === "string") { + return linkSchema.safeParse(response).success; + } + return true; }); }; return (
-
+
{/* Banner */} {form.banner &&
} {/* Header */}
- {/* Implement View/Edit Title */} - {/*

{`${allowEdit ? "Edit" : "View"} - ${form.name}`}

*/} -

{`${"View"} - ${form.name}`}

+

{`${allowEdit ? "Edit" : "View"} - ${form.name}`}

{form.description && (

{form.description}

@@ -240,39 +352,76 @@ export function FormReviewClient({
- {/* Questions */} + {/* Questions and Instructions */}
- {form.questions.map((q, index) => { - const questionText = q.question; - const responseValue: - | string - | string[] - | number - | Date - | null - | undefined = responses[questionText]; - return ( -
- { - handleResponseChange(questionText, value); - }} - disabled={formDisabled} - /> -
+ {(() => { + /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */ + // Combine questions and instructions, sort by order + type QuestionWithOrder = (typeof form.questions)[number] & { + itemType: "question"; + }; + interface InstructionWithOrder { + itemType: "instruction"; + title: string; + content?: string; + imageUrl?: string; + videoUrl?: string; + order?: number; + } + + const questionsWithType: QuestionWithOrder[] = form.questions.map( + (q) => ({ + ...q, + itemType: "question" as const, + }), ); - })} + + const instructionsWithType: InstructionWithOrder[] = ( + (form as any).instructions || [] + ).map((inst: any) => ({ + ...inst, + itemType: "instruction" as const, + })); + + const allItems = [ + ...questionsWithType, + ...instructionsWithType, + ].sort((a, b) => (a.order ?? 999) - (b.order ?? 999)); + + return allItems.map((item, index) => { + const isInstruction = item.itemType === "instruction"; + + return ( +
+ {isInstruction ? ( + + ) : ( + { + handleResponseChange(item.question, value); + }} + onBlur={() => handleFieldBlur(item.question)} + formId={formQuery.data.id} + error={getValidationError(item)} + disabled={!allowEdit} + /> + )} +
+ ); + }); + /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */ + })()}
{submitError && ( @@ -283,23 +432,15 @@ export function FormReviewClient({ {/* Action Buttons */}
- {/* Implement disabling form */} - {/* {!formDisabled && ( + {allowEdit && ( - )} */} - + )}
diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 26303596..9b585165 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -477,6 +477,32 @@ export const formsRouter = { }); }), + editResponse: protectedProcedure + .input( + InsertFormResponseSchema.omit({ userId: true, form: true }).extend({ + id: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const userId = ctx.session.user.id; + + const updated = await db + .update(FormResponse) + .set({ responseData: input.responseData, editedAt: new Date() }) + .where( + and(eq(FormResponse.id, input.id), eq(FormResponse.userId, userId)), + ) + .returning({ id: FormResponse.id, editedAt: FormResponse.editedAt }); + + if (updated.length === 0) { + throw new TRPCError({ + message: "Form response edit failed", + code: "BAD_REQUEST", + }); + } + return updated[0]; + }), + getResponses: permProcedure .input(z.object({ form: z.string() })) .query(async ({ input, ctx }) => { @@ -534,12 +560,13 @@ export const formsRouter = { if (responseId) { return await db .select({ - submittedAt: FormResponse.createdAt, + submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, + allowEdit: sql`false`, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) @@ -551,40 +578,41 @@ export const formsRouter = { ); } - // return all responses all forms + // return all responses of form const form = input.form; - if (!form) { + if (form) { return await db - .select({ - submittedAt: FormResponse.createdAt, + .select({ + submittedAt: FormResponse.editedAt, + responseData: FormResponse.responseData, + formName: FormsSchemas.name, + formSlug: FormsSchemas.slugName, + id: FormResponse.id, + hasSubmitted: sql`true`, + allowEdit: sql`false`, + }) + .from(FormResponse) + .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) + .where(and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form))) + .orderBy(desc(FormResponse.editedAt)); + + } + + // return all responses all forms + return await db + .select({ + submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, + allowEdit: FormsSchemas.allowEdit, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) .where(eq(FormResponse.userId, userId)) - .orderBy(desc(FormResponse.createdAt)); - } - - // return all responses of form - return await db - .select({ - submittedAt: FormResponse.createdAt, - responseData: FormResponse.responseData, - formName: FormsSchemas.name, - formSlug: FormsSchemas.slugName, - id: FormResponse.id, - hasSubmitted: sql`true`, - }) - .from(FormResponse) - .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) - .where( - and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form)), - ) - .orderBy(desc(FormResponse.createdAt)); + .orderBy(desc(FormResponse.editedAt)); }), // Generate presigned upload URL for direct MinIO upload From 8334ec537bbfd6a811f332a4f3d6586f95322e61 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 18:09:29 -0500 Subject: [PATCH 6/8] format and lint --- .../src/app/admin/forms/[slug]/client.tsx | 4 +- .../member-dashboard/forms/form-responses.tsx | 10 +++-- .../_components/form-view-edit-client.tsx | 5 ++- .../_components/question-response-card.tsx | 8 ++-- packages/api/src/routers/forms.ts | 43 ++++++++++--------- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 07c75528..b36d088b 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -550,7 +550,7 @@ export function EditorClient({
-
+
-
- - + +
@@ -51,4 +55,4 @@ export async function FormResponses() { )} ); -} \ No newline at end of file +} diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index b4b370cc..914fd219 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -26,12 +26,13 @@ interface FormReviewClientProps { export function FormReviewClient({ formName, userName, + // eslint-disable-next-line @typescript-eslint/no-unused-vars handleCallbacks, responseId, }: FormReviewClientProps) { const router = useRouter(); const [responses, setResponses] = useState< - Record + Record >({}); const [touchedFields, setTouchedFields] = useState>(new Set()); const [isSubmitted, setIsSubmitted] = useState(false); @@ -125,7 +126,7 @@ export function FormReviewClient({ }, [isSubmitted, router]); // wait for all queries to load - if (formQuery.isLoading || responseQuery.isLoading || responses === null) + if (formQuery.isLoading || responseQuery.isLoading) return (
diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx index 929be1a5..6c1b4e1e 100644 --- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx @@ -34,7 +34,7 @@ type FormQuestion = z.infer; interface QuestionResponseCardProps { question: FormQuestion; - value?: string | string[] | number | Date | Boolean| null; + value?: string | string[] | number | Date | boolean | null; onChange: (value: string | string[] | number | Date | null) => void; onBlur?: () => void; disabled?: boolean; @@ -102,7 +102,7 @@ function QuestionBody({ formId, }: { question: FormQuestion; - value?: string | string[] | number | Date | Boolean | null; + value?: string | string[] | number | Date | boolean | null; onChange: (value: string | string[] | number | Date | null) => void; onBlur?: () => void; disabled?: boolean; @@ -753,9 +753,9 @@ function FileUploadInput({ const getUploadUrlMutation = api.forms.getUploadUrl.useMutation(); - // used to sync with responseData for view/edit, otherwise value will be null + // used to sync with responseData for view/edit, otherwise value will be null React.useEffect(() => { - setFileName(value ? value.split("/").pop() ?? null : null); + setFileName(value ? (value.split("/").pop() ?? null) : null); }, [value]); const handleFileUpload = async (e: React.ChangeEvent) => { diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 9b585165..661ebe5e 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -582,37 +582,38 @@ export const formsRouter = { const form = input.form; if (form) { return await db - .select({ - submittedAt: FormResponse.editedAt, - responseData: FormResponse.responseData, - formName: FormsSchemas.name, - formSlug: FormsSchemas.slugName, - id: FormResponse.id, - hasSubmitted: sql`true`, - allowEdit: sql`false`, - }) - .from(FormResponse) - .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) - .where(and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form))) - .orderBy(desc(FormResponse.editedAt)); - - } - - // return all responses all forms - return await db - .select({ + .select({ submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, - allowEdit: FormsSchemas.allowEdit, + allowEdit: sql`false`, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) - .where(eq(FormResponse.userId, userId)) + .where( + and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form)), + ) .orderBy(desc(FormResponse.editedAt)); + } + + // return all responses all forms + return await db + .select({ + submittedAt: FormResponse.editedAt, + responseData: FormResponse.responseData, + formName: FormsSchemas.name, + formSlug: FormsSchemas.slugName, + id: FormResponse.id, + hasSubmitted: sql`true`, + allowEdit: FormsSchemas.allowEdit, + }) + .from(FormResponse) + .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) + .where(eq(FormResponse.userId, userId)) + .orderBy(desc(FormResponse.editedAt)); }), // Generate presigned upload URL for direct MinIO upload From cd55ef89249d8052b85be6d13156429a313a4ef3 Mon Sep 17 00:00:00 2001 From: aiden Date: Sat, 31 Jan 2026 18:39:16 -0500 Subject: [PATCH 7/8] removing proc connections for now --- .../forms/[formName]/[responseId]/page.tsx | 22 ++++++++++++++++++- .../_components/form-view-edit-client.tsx | 2 -- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx b/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx index bba2adef..682bc43b 100644 --- a/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx +++ b/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx @@ -1,6 +1,8 @@ import { redirect } from "next/navigation"; +import { XCircle } from "lucide-react"; import { auth } from "@forge/auth"; +import { Card } from "@forge/ui/card"; import { SIGN_IN_PATH } from "~/consts"; import { api, HydrateClient } from "~/trpc/server"; @@ -16,8 +18,26 @@ export default async function FormResponderPage({ redirect(SIGN_IN_PATH); } + if (!params.formName) { + return ( +
+ + +

Form not found

+
+
+ ); + } + if (!params.responseId) { - return
Submission not found
; + return ( +
+ + +

Response not found

+
+
+ ); } // handle url encode form names to allow spacing and special characters diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 914fd219..4913d076 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -19,7 +19,6 @@ const linkSchema = z.string().url("Please enter a valid URL"); interface FormReviewClientProps { formName: string; userName: string; - handleCallbacks: (response: Record) => void; responseId: string; } @@ -27,7 +26,6 @@ export function FormReviewClient({ formName, userName, // eslint-disable-next-line @typescript-eslint/no-unused-vars - handleCallbacks, responseId, }: FormReviewClientProps) { const router = useRouter(); From 4a66c117f15f2ed269e5cc87514883f369d37a6a Mon Sep 17 00:00:00 2001 From: aiden Date: Sun, 1 Feb 2026 18:46:08 -0500 Subject: [PATCH 8/8] ensure connections cant be added to forms w/ edit --- packages/api/src/routers/forms.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 661ebe5e..1727d6c3 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -326,6 +326,18 @@ export const formsRouter = { ) .mutation(async ({ input, ctx }) => { controlPerms.or(["EDIT_FORMS"], ctx); + + const form = await db.query.FormsSchemas.findFirst({ + where: (t, { eq }) => eq(t.id, input.form), + }); + + if (form?.allowEdit) { + throw new TRPCError({ + message: "Cannot add connection to form with allowEdit", + code: "BAD_REQUEST", + }); + } + try { await db.insert(TrpcFormConnection).values({ ...input }); } catch {