From a4c4a72dbc7f68593b4611f4090ccd676f6db9e8 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 11:37:47 +0100 Subject: [PATCH] feat(web): StepPickerModal step picker component --- web/components/workflows/StepPickerModal.tsx | 191 +++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 web/components/workflows/StepPickerModal.tsx diff --git a/web/components/workflows/StepPickerModal.tsx b/web/components/workflows/StepPickerModal.tsx new file mode 100644 index 0000000..17a4967 --- /dev/null +++ b/web/components/workflows/StepPickerModal.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useMemo, useRef, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { api, WorkflowStep } from "@/lib/api"; +import { Modal } from "@/components/ui"; + +type Tab = "all" | "bash" | "powershell" | "adhoc"; + +function ShellBadge({ interpreter }: { interpreter: "bash" | "powershell" }) { + const isBash = interpreter === "bash"; + return ( + + {isBash ? "bash" : "pwsh"} + + ); +} + +function DefaultBadge() { + return ( + + default + + ); +} + +function StepCard({ step, onAdd }: { step: WorkflowStep; onAdd: () => void }) { + return ( + + ); +} + +export function StepPickerModal({ + open, + onClose, + onSelect, + onAddAdhoc, + onImportAdhoc, +}: { + open: boolean; + onClose: () => void; + onSelect: (stepId: string) => void; + onAddAdhoc: () => void; + onImportAdhoc: (file: File) => void; +}) { + const { data: library } = useQuery({ queryKey: ["steps"], queryFn: api.listSteps }); + const [search, setSearch] = useState(""); + const [tab, setTab] = useState("all"); + const fileRef = useRef(null); + + const filtered = useMemo(() => { + const q = search.toLowerCase(); + return (library ?? []).filter( + (s) => + (s.name.toLowerCase().includes(q) || (s.description ?? "").toLowerCase().includes(q)) && + (tab === "all" || tab === "adhoc" ? true : s.interpreter === tab), + ); + }, [library, search, tab]); + + const group = (source: "default" | "shared", interp: "bash" | "powershell") => + filtered.filter( + (s) => s.interpreter === interp && (source === "default" ? s.source === "default" : s.source !== "default"), + ); + + const groups: { label: string; steps: WorkflowStep[] }[] = [ + { label: "Default · Bash", steps: group("default", "bash") }, + { label: "Default · PowerShell", steps: group("default", "powershell") }, + { label: "Shared · Bash", steps: group("shared", "bash") }, + { label: "Shared · PowerShell", steps: group("shared", "powershell") }, + ]; + + const showLibrary = tab !== "adhoc"; + const showAdhocCards = tab === "all" || tab === "adhoc"; + + return ( + +
+ setSearch(e.target.value)} + /> + +
+ {(["all", "bash", "powershell", "adhoc"] as Tab[]).map((t) => ( + + ))} +
+ + {showAdhocCards && ( +
+ + + { + const f = e.target.files?.[0]; + if (f) onImportAdhoc(f); + e.target.value = ""; + }} + /> +
+ )} + + {showLibrary && + groups.map( + (g) => + g.steps.length > 0 && ( +
+
+ {g.label} + +
+
+ {g.steps.map((s) => ( + onSelect(s.step_id)} /> + ))} +
+
+ ), + )} + + {showLibrary && filtered.length === 0 && ( +

No steps match your search.

+ )} + +

+ Click a card to append it to the workflow · manage the library on the{" "} + + Steps + {" "} + page. +

+
+
+ ); +}