From 3584c72055bf75c12665e60984dead2d8f405613 Mon Sep 17 00:00:00 2001 From: Jesus Gonzalez Date: Tue, 3 Mar 2026 11:51:33 -0500 Subject: [PATCH 1/5] Added isClosed to FormsSchema --- packages/db/src/schemas/knight-hacks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts index adade6209..bcb310505 100644 --- a/packages/db/src/schemas/knight-hacks.ts +++ b/packages/db/src/schemas/knight-hacks.ts @@ -495,6 +495,7 @@ export const FormsSchemas = createTable("form_schemas", (t) => ({ sectionId: t .uuid() .references(() => FormSections.id, { onDelete: "set null" }), + isClosed: t.boolean().notNull().default(false), })); export type Form = typeof FormsSchemas.$inferSelect; From 85bbea8089f521fe714aba04e32f9838f656d81b Mon Sep 17 00:00:00 2001 From: Jesus Gonzalez Date: Wed, 4 Mar 2026 09:28:30 -0500 Subject: [PATCH 2/5] Added toggleFormClosed router Added isClosed state to Form Editor Added "Form Closed" gate in form responder. Added "Closed" tag to forms in dashboard. Reformatted Form Options into Dropdown Ran Format, Lint, and Typecheck --- .../_components/admin/forms/editor/client.tsx | 99 +++++++++++-------- .../member-dashboard/forms/form-responses.tsx | 10 +- .../forms/form-responder-client.tsx | 15 +++ .../forms/form-view-edit-client.tsx | 2 +- packages/api/src/routers/forms.ts | 54 ++++++++++ 5 files changed, 137 insertions(+), 43 deletions(-) diff --git a/apps/blade/src/app/_components/admin/forms/editor/client.tsx b/apps/blade/src/app/_components/admin/forms/editor/client.tsx index f87972a1b..0eba60891 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/client.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/client.tsx @@ -22,7 +22,7 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { ArrowLeft, Loader2, Plus, Save, Users } from "lucide-react"; +import { ArrowLeft, CogIcon, Loader2, Plus, Save, Users } from "lucide-react"; import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; @@ -37,9 +37,17 @@ import { DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@forge/ui/dropdown-menu"; import { Input } from "@forge/ui/input"; import { Label } from "@forge/ui/label"; -import { Switch } from "@forge/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { Textarea } from "@forge/ui/textarea"; @@ -194,6 +202,7 @@ export function EditorClient({ const [isLoading, setIsLoading] = useState(true); const [saveStatus, setSaveStatus] = useState(""); + const [isClosed, setIsClosed] = useState(false); const { data: formData, @@ -251,6 +260,7 @@ export function EditorClient({ allowResubmission, allowEdit, responseRoleIds, + isClosed, }); }, [ isLoading, @@ -267,6 +277,7 @@ export function EditorClient({ allowResubmission, allowEdit, responseRoleIds, + isClosed, ]); const saveFormRef = React.useRef(handleSaveForm); @@ -299,6 +310,7 @@ export function EditorClient({ setAllowResubmission(formData.allowResubmission); setAllowEdit(formData.allowEdit); setResponseRoleIds(formData.responseRoleIds); + setIsClosed(formData.isClosed); const formQuestions = Array.isArray(loadedFormData.questions) ? loadedFormData.questions @@ -331,7 +343,14 @@ export function EditorClient({ // auto save trigger when toggle switches are changed useEffect(() => { if (!isLoading) saveFormRef.current(); - }, [duesOnly, allowResubmission, responseRoleIds, isLoading, allowEdit]); + }, [ + duesOnly, + allowResubmission, + responseRoleIds, + isLoading, + allowEdit, + isClosed, + ]); // auto save when finishing editing an item (changing active card) useEffect(() => { @@ -555,43 +574,43 @@ export function EditorClient({
- - -
-
- - -
-
- - + + + + + + + Form Settings + + + Dues Only + + + Allow Multiple Responses + + + Allow Response Edit + + + Form Closed + + + +
- + {formResponse.isClosed && ( + + Closed + + )} {formResponse.formSlug && ( )} diff --git a/apps/blade/src/app/_components/forms/form-responder-client.tsx b/apps/blade/src/app/_components/forms/form-responder-client.tsx index aeb2ccba3..467f23849 100644 --- a/apps/blade/src/app/_components/forms/form-responder-client.tsx +++ b/apps/blade/src/app/_components/forms/form-responder-client.tsx @@ -83,6 +83,7 @@ export function FormResponderWrapper({ const isDuesOnly = formQuery.data.duesOnly; const allowResubmission = formQuery.data.allowResubmission; const allowEdit = formQuery.data.allowEdit; + const isClosed = formQuery.data.isClosed; const duesCheckFailed = !!duesQuery.error; const hasPaidDues = duesCheckFailed @@ -106,6 +107,20 @@ export function FormResponderWrapper({ ); } + if (isClosed) { + return ( +
+ + +

Form Closed

+

+ This form is no longer accepting responses. +

+
+
+ ); + } + // already submitted gate if (hasAlreadySubmitted && !allowResubmission) { const existing = existingResponseQuery.data?.[0]; diff --git a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx index 2a45e2652..f317ca349 100644 --- a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx +++ b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx @@ -74,7 +74,7 @@ export function FormReviewWrapper({ const zodValidator = form.zodValidator; - const allowEdit = form.allowEdit; + const allowEdit = form.allowEdit && !form.isClosed; // success if (isSubmitted) { diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 86c5195b3..70a53702d 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -357,6 +357,13 @@ export const formsRouter = { }); } + if (form.isClosed) { + throw new TRPCError({ + message: "This form is closed and no longer accepting responses", + code: "FORBIDDEN", + }); + } + const responseRoles = await db .select({ roleId: FormResponseRoles.roleId }) .from(FormResponseRoles) @@ -476,6 +483,13 @@ export const formsRouter = { where: (t, { eq }) => eq(t.id, existingResponse.form), }); + if (form?.isClosed) { + throw new TRPCError({ + message: "This form is closed and no longer accepting responses", + code: "FORBIDDEN", + }); + } + if (!form?.allowEdit) { throw new TRPCError({ message: "This form does not allow editing responses", @@ -585,6 +599,7 @@ export const formsRouter = { id: FormResponse.id, hasSubmitted: sql`true`, allowEdit: FormsSchemas.allowEdit, + isClosed: FormsSchemas.isClosed, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) @@ -608,6 +623,7 @@ export const formsRouter = { id: FormResponse.id, hasSubmitted: sql`true`, allowEdit: FormsSchemas.allowEdit, + isClosed: FormsSchemas.isClosed, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) @@ -627,6 +643,7 @@ export const formsRouter = { id: FormResponse.id, hasSubmitted: sql`true`, allowEdit: FormsSchemas.allowEdit, + isClosed: FormsSchemas.isClosed, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) @@ -1312,4 +1329,41 @@ export const formsRouter = { return { canEdit: hasSectionRole }; }), + toggleFormClosed: permProcedure + .input( + z.object({ + slug_name: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + controlPerms.or(["EDIT_FORMS"], ctx); + + // Get the form + const form = await db.query.FormsSchemas.findFirst({ + where: (t, { eq }) => + eq(t.slugName, decodeURIComponent(input.slug_name)), + }); + + // Validate if we got the form + if (!form) { + throw new TRPCError({ message: "Form not found", code: "NOT_FOUND" }); + } + + // Update the forms isClosed state + const newState = !form.isClosed; + + await db + .update(FormsSchemas) + .set({ isClosed: newState }) + .where(eq(FormsSchemas.id, form.id)); + + await log({ + title: `Form ${newState ? "Closed" : "Opened"}`, + message: `**Form** ${form.name}`, + color: newState ? "uhoh_red" : "success_green", + userId: ctx.session.user.discordUserId, + }); + + return { isClosed: newState }; + }), } satisfies TRPCRouterRecord; From 18905ab9c34a8701b49003e5978b4ca52f07f444 Mon Sep 17 00:00:00 2001 From: Jesus Gonzalez Date: Wed, 4 Mar 2026 12:09:27 -0500 Subject: [PATCH 3/5] Addressed Code Rabbit Issues and PR test Issue --- .../_components/admin/forms/editor/client.tsx | 8 ++++++- packages/api/src/routers/forms.ts | 24 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/blade/src/app/_components/admin/forms/editor/client.tsx b/apps/blade/src/app/_components/admin/forms/editor/client.tsx index 0eba60891..9ca1ab7e0 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/client.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/client.tsx @@ -576,7 +576,13 @@ export function EditorClient({
- diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 70a53702d..4298d3f3f 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1350,20 +1350,26 @@ export const formsRouter = { } // Update the forms isClosed state - const newState = !form.isClosed; - - await db + const [updatedForm] = await db .update(FormsSchemas) - .set({ isClosed: newState }) - .where(eq(FormsSchemas.id, form.id)); + .set({ isClosed: sql`NOT ${FormsSchemas.isClosed}` }) + .where(eq(FormsSchemas.id, form.id)) + .returning({ + isClosed: FormsSchemas.isClosed, + name: FormsSchemas.name, + }); + + if (!updatedForm) { + throw new TRPCError({ message: "Form not found", code: "NOT_FOUND" }); + } await log({ - title: `Form ${newState ? "Closed" : "Opened"}`, - message: `**Form** ${form.name}`, - color: newState ? "uhoh_red" : "success_green", + title: `Form ${updatedForm.isClosed ? "closed" : "opened"}`, + message: `**Form:** ${updatedForm.name}`, + color: updatedForm.isClosed ? "uhoh_red" : "success_green", userId: ctx.session.user.discordUserId, }); - return { isClosed: newState }; + return { isClosed: updatedForm.isClosed }; }), } satisfies TRPCRouterRecord; From cd02c13722e091e96b88bf1f97109ef65498e61e Mon Sep 17 00:00:00 2001 From: Jesus Gonzalez Date: Wed, 4 Mar 2026 12:11:28 -0500 Subject: [PATCH 4/5] Ran Format, Lint, and Typecheck --- apps/blade/src/app/_components/admin/forms/editor/client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/blade/src/app/_components/admin/forms/editor/client.tsx b/apps/blade/src/app/_components/admin/forms/editor/client.tsx index 9ca1ab7e0..3eecdca26 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/client.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/client.tsx @@ -576,7 +576,7 @@ export function EditorClient({
-