diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index cf8960e05..e50afeea0 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -25,6 +25,7 @@ import { CSS } from "@dnd-kit/utilities"; import { Loader2, Plus, Save } from "lucide-react"; import type { + FormType, InstructionValidator, QuestionValidator, } from "@forge/consts/knight-hacks"; @@ -33,11 +34,16 @@ import { Card } from "@forge/ui/card"; 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"; +import type { MatchingType } from "./linker"; +import type { ProcedureMeta } from "~/lib/utils"; import { InstructionEditCard } from "~/components/admin/forms/instruction-edit-card"; import { QuestionEditCard } from "~/components/admin/forms/question-edit-card"; import { api } from "~/trpc/react"; +import { ConnectionViewer } from "./con-viewer"; +import ListMatcher from "./linker"; type FormQuestion = z.infer; type FormInstruction = z.infer; @@ -111,7 +117,39 @@ function SortableItem({ ); } -export function EditorClient({ slug }: { slug: string }) { +function ConnectionsTab(props: { + procs: Record; + slug: string; + id: string; + formData: FormType; +}) { + const questions = props.formData.questions.map((q) => q.question); + const { data: connections } = api.forms.getConnections.useQuery({ + id: props.id, + }); + return ( + <> + + {connections?.map((con) => { + return ( + + ); + })} + + ); +} + +export function EditorClient({ + procs, + slug, +}: { + procs: Record; + slug: string; +}) { const router = useRouter(); const [formTitle, setFormTitle] = useState(""); @@ -472,68 +510,91 @@ export function EditorClient({ slug }: { slug: string }) { - - -
- {[...questions, ...instructions] - .sort((a, b) => (a.order ?? 999) - (b.order ?? 999)) - .map((item) => { - const isInstruction = "title" in item; - return ( -
- setActiveItemId(item.id)} - onForceSave={handleSaveForm} - error={ - !isInstruction && duplicateIds.has(item.id) - ? "Duplicate question title" - : undefined - } - /> -
- ); - })} + + + Questions + Connections + + + + + +
+ {[...questions, ...instructions] + .sort((a, b) => (a.order ?? 999) - (b.order ?? 999)) + .map((item) => { + const isInstruction = "title" in item; + return ( +
+ setActiveItemId(item.id)} + onForceSave={handleSaveForm} + error={ + !isInstruction && duplicateIds.has(item.id) + ? "Duplicate question title" + : undefined + } + /> +
+ ); + })} +
+
+
+ +
+ +
- - - -
- - -
+
+ + + {formData && ( + + )} + +
); diff --git a/apps/blade/src/app/admin/forms/[slug]/con-viewer.tsx b/apps/blade/src/app/admin/forms/[slug]/con-viewer.tsx new file mode 100644 index 000000000..0765c02ed --- /dev/null +++ b/apps/blade/src/app/admin/forms/[slug]/con-viewer.tsx @@ -0,0 +1,144 @@ +import { useState } from "react"; +import { Trash2 } from "lucide-react"; + +import { Button } from "@forge/ui/button"; +import { Card } from "@forge/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@forge/ui/dialog"; +import { toast } from "@forge/ui/toast"; + +import type { MatchingType } from "./linker"; +import { api } from "~/trpc/react"; + +export function ConnectionViewer({ + matching, + form_slug, +}: { + matching: MatchingType & { id: string }; + form_slug: string; +}) { + const [open, setOpen] = useState(false); + const utils = api.useUtils(); + + const handleDeleteCon = api.forms.deleteConnection.useMutation({ + onSuccess() { + toast.success("Connection deleted"); + setOpen(false); + }, + onError() { + toast.error("Failed to delete connection"); + }, + async onSettled() { + await utils.forms.getConnections.invalidate(); + }, + }); + + const handleDelete = () => { + handleDeleteCon.mutate({ id: matching.id }); + }; + + return ( + +
+
+
+
+
+
+ Procedure +
+
+ {matching.proc || "No Procedure"} +
+
+
+
+
+ Form +
+
+ {form_slug || "No Form"} +
+
+
+ +
+
+ Field Mappings ({matching.connections.length}) +
+
+ {matching.connections.map((conn, idx) => ( +
+
+
+ Proc Field +
+
+ {conn.procField} +
+
+
+
+
+ Form Field +
+
+ {conn.formField || "Not Mapped"} +
+
+
+ ))} +
+
+
+ + + + + + + + Delete Connection? + + This will permanently delete the connection between{" "} + + {matching.proc || "the procedure"} + {" "} + and{" "} + + {form_slug || "the form"} + + . This action cannot be undone. + + + + + + + + +
+
+
+ ); +} diff --git a/apps/blade/src/app/admin/forms/trpc/_components/picker.tsx b/apps/blade/src/app/admin/forms/[slug]/linker.tsx similarity index 80% rename from apps/blade/src/app/admin/forms/trpc/_components/picker.tsx rename to apps/blade/src/app/admin/forms/[slug]/linker.tsx index 8f2509892..f14f0d003 100644 --- a/apps/blade/src/app/admin/forms/trpc/_components/picker.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/linker.tsx @@ -30,22 +30,25 @@ const matchingSchema = z.object({ ), }); +export type MatchingType = z.infer; + export default function ListMatcher({ procs, - forms, + form, }: { procs: Record; - forms: Record; + form: { questions: string[]; id: string }; }) { + const utils = api.useUtils(); const [procSelection, setProcSelection] = useState(""); - const [formSelection, setFormSelection] = useState(""); const [procFields, setProcFields] = useState([]); - const [formFields, setFormFields] = useState([]); const [isLoading, setIsLoading] = useState(false); const [connections, setConnections] = useState< { procField: string; formField: string }[] >([]); + const formFields = form.questions; + const addConnection = api.forms.addConnection.useMutation({ onSuccess() { toast.success("Added connection"); @@ -53,8 +56,9 @@ export default function ListMatcher({ onError() { toast.error("Failed to add connections"); }, - onSettled() { + async onSettled() { setIsLoading(false); + await utils.forms.getConnections.invalidate(); }, }); @@ -68,13 +72,6 @@ export default function ListMatcher({ ); }; - const handleFormChange = (value: string) => { - if (!forms[value]) return; - setFormSelection(value); - setFormFields(forms[value].questions); - setConnections((prev) => prev.map((conn) => ({ ...conn, formField: "" }))); - }; - const updateConnection = (index: number, value: string) => { setConnections((prev) => { const updated = [...prev]; @@ -94,10 +91,9 @@ export default function ListMatcher({ const handleSubmit = () => { setIsLoading(true); - if (!forms[formSelection]) return; const data = { - form: forms[formSelection].id, + form: form.id, proc: procSelection, connections: connections, }; @@ -111,7 +107,7 @@ export default function ListMatcher({ }; return ( -
+
@@ -128,22 +124,6 @@ export default function ListMatcher({
- -
- - -
{procFields.length > 0 && formFields.length > 0 && ( diff --git a/apps/blade/src/app/admin/forms/[slug]/page.tsx b/apps/blade/src/app/admin/forms/[slug]/page.tsx index 4cc01b463..2e41c8a77 100644 --- a/apps/blade/src/app/admin/forms/[slug]/page.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/page.tsx @@ -1,7 +1,9 @@ import { redirect } from "next/navigation"; +import { appRouter } from "@forge/api"; import { auth } from "@forge/auth/server"; +import { extractProcedures } from "~/lib/utils"; import { api } from "~/trpc/server"; import { EditorClient } from "./client"; @@ -24,5 +26,7 @@ export default async function FormEditorPage({ } } - return ; + return ( + + ); } diff --git a/apps/blade/src/app/admin/forms/trpc/page.tsx b/apps/blade/src/app/admin/forms/trpc/page.tsx deleted file mode 100644 index 9ca306390..000000000 --- a/apps/blade/src/app/admin/forms/trpc/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { FormType } from "@forge/consts/knight-hacks"; -import { appRouter } from "@forge/api"; - -import { extractProcedures } from "~/lib/utils"; -import { api } from "~/trpc/server"; -import ListMatcher from "./_components/picker"; - -export default async function Page() { - const forms = await api.forms.getAllForms(); - const allForms = Object.fromEntries( - forms.map((form) => [ - form.slugName, - { - questions: (form.formData as FormType).questions.map((q) => q.question), - id: form.id, - }, - ]), - ); - - return ; -} diff --git a/apps/blade/src/app/forms/[formName]/page.tsx b/apps/blade/src/app/forms/[formName]/page.tsx index 3355a11a0..896f70063 100644 --- a/apps/blade/src/app/forms/[formName]/page.tsx +++ b/apps/blade/src/app/forms/[formName]/page.tsx @@ -48,7 +48,8 @@ export default async function FormResponderPage({ procField: string; formField: string; }[]) { - data[map.procField] = response[map.formField]; + if (map.formField in response) + data[map.procField] = response[map.formField]; } const route = procs[con.proc]?.route.split("."); diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index e05dfabe6..ef84a0438 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -208,11 +208,6 @@ export const formsRouter = { }; }), - getAllForms: protectedProcedure.query(async () => { - const forms = await db.query.FormsSchemas.findMany(); - return forms; - }), - addConnection: adminProcedure .input(TrpcFormConnectionSchema) .mutation(async ({ input }) => { @@ -228,7 +223,7 @@ export const formsRouter = { getConnections: protectedProcedure .input(z.object({ id: z.string() })) - .mutation(async ({ input }) => { + .query(async ({ input }) => { try { const connections = db.query.TrpcFormConnection.findMany({ where: (t, { eq }) => eq(t.form, input.id), @@ -242,6 +237,21 @@ export const formsRouter = { } }), + deleteConnection: adminProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input }) => { + try { + await db + .delete(TrpcFormConnection) + .where(eq(TrpcFormConnection.id, input.id)); + } catch { + throw new TRPCError({ + message: "Could not delete connection", + code: "BAD_REQUEST", + }); + } + }), + createResponse: protectedProcedure .input(InsertFormResponseSchema.omit({ userId: true })) .mutation(async ({ input, ctx }) => {