diff --git a/web/app/workflows/[id]/page.tsx b/web/app/workflows/[id]/page.tsx index 394586a..35983e7 100644 --- a/web/app/workflows/[id]/page.tsx +++ b/web/app/workflows/[id]/page.tsx @@ -3,11 +3,11 @@ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { api, Workflow, WorkflowStep, WorkflowStepRef, SecretGroupSummary } from "@/lib/api"; import { Button } from "@/components/ui"; -import { EditStepModal } from "@/components/workflows/EditStepModal"; import { EditWorkflowModal } from "@/components/workflows/EditWorkflowModal"; +import { StepPickerModal } from "@/components/workflows/StepPickerModal"; const inputClass = "w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder-text-secondary/50 focus:border-signal focus:outline-none focus:ring-1 focus:ring-signal"; @@ -27,14 +27,6 @@ function ShellBadge({ interpreter }: { interpreter: "bash" | "powershell" }) { ); } -function DefaultBadge() { - return ( - - default - - ); -} - function AdhocBadge() { return ( @@ -57,11 +49,9 @@ export default function WorkflowBuilder() { const params = useParams<{ id: string }>(); const id = params.id; const router = useRouter(); - const queryClient = useQueryClient(); const [wf, setWf] = useState(null); const [selected, setSelected] = useState(null); - const [search, setSearch] = useState(""); const [saving, setSaving] = useState(false); const [lastSaved, setLastSaved] = useState(null); const [, setTick] = useState(0); @@ -69,16 +59,11 @@ export default function WorkflowBuilder() { const [running, setRunning] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); - const [importing, setImporting] = useState(false); const [importingInline, setImportingInline] = useState(false); - const [syncing, setSyncing] = useState(false); - const fileInputRef = useRef(null); - const inlineFileInputRef = useRef(null); const [groupKeys, setGroupKeys] = useState>({}); const [editWorkflowOpen, setEditWorkflowOpen] = useState(false); - const [editingStep, setEditingStep] = useState(null); - const [editStepOpen, setEditStepOpen] = useState(false); const [dragOverZone, setDragOverZone] = useState(null); + const [pickerOpen, setPickerOpen] = useState(false); const { data: loaded } = useQuery({ queryKey: ["workflow", id], @@ -212,23 +197,6 @@ export default function WorkflowBuilder() { }); }; - const onImportInline = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - setImportingInline(true); - setError(null); - try { - const doc = JSON.parse(await file.text()); - const step = await api.parseStep(doc); - appendRef({ inline: step, order: wf.steps.length, on_failure: "stop", max_retries: 0 }); - } catch (err) { - setError((err as Error).message); - } finally { - setImportingInline(false); - e.target.value = ""; - } - }; - const moveStep = (from: number, pos: number) => { const next = [...sortedSteps]; const [item] = next.splice(from, 1); @@ -291,42 +259,6 @@ export default function WorkflowBuilder() { updateRef(selectedIdxInWf, { overrides: { ...selectedRef.overrides, secret_refs: next } }); }; - const onImportFile = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - setImporting(true); - setError(null); - try { - const doc = JSON.parse(await file.text()); - await api.importStep(doc); - await queryClient.invalidateQueries({ queryKey: ["steps"] }); - setNotice("Step imported."); - } catch (err) { - setError((err as Error).message); - } finally { - setImporting(false); - e.target.value = ""; - } - }; - - const onSyncDefaults = async () => { - setSyncing(true); - setError(null); - try { - const { created, updated } = await api.seedDefaults(); - await queryClient.invalidateQueries({ queryKey: ["steps"] }); - setNotice(`${created} created, ${updated} updated`); - } catch (err) { - setError((err as Error).message); - } finally { - setSyncing(false); - } - }; - - const filteredLibrary = (library ?? []).filter((s) => s.name.toLowerCase().includes(search.toLowerCase())); - const bashSteps = filteredLibrary.filter((s) => s.interpreter === "bash"); - const pwshSteps = filteredLibrary.filter((s) => s.interpreter === "powershell"); - const upstreamOutputsFor = (i: number) => Array.from( new Set( @@ -390,104 +322,19 @@ export default function WorkflowBuilder() { {notice} )} - - {/* LEFT: library */} - - + {/* CENTER: canvas */} + + setPickerOpen(true)} + className="pointer-events-auto inline-flex items-center gap-2 rounded-[9px] bg-signal px-4 py-2.5 text-sm font-semibold text-signal-ink shadow-[0_6px_20px_rgba(245,165,36,0.28)] hover:bg-signal/90" + > + + Add step + + {sortedSteps.map((ref, i) => { @@ -548,9 +395,10 @@ export default function WorkflowBuilder() { e.preventDefault()} onDrop={(e) => handleDrop(e, 0)} + onClick={() => setPickerOpen(true)} className="mt-2 w-full rounded-[10px] border border-dashed border-border bg-surface py-6 text-sm text-text-secondary hover:border-signal/50 hover:text-text-primary" > - + Drop a step here + + Add your first step )} @@ -757,55 +605,29 @@ export default function WorkflowBuilder() { setWf(w)} onClose={() => setEditWorkflowOpen(false)} /> - { - setEditStepOpen(false); - queryClient.invalidateQueries({ queryKey: ["steps"] }); + setPickerOpen(false)} + onSelect={(stepId) => insertLibStep(stepId, sortedSteps.length)} + onAddAdhoc={() => { + addAdhocStep(); + setPickerOpen(false); + }} + onImportAdhoc={async (file) => { + setPickerOpen(false); + setImportingInline(true); + setError(null); + try { + const doc = JSON.parse(await file.text()); + const step = await api.parseStep(doc); + appendRef({ inline: step, order: wf.steps.length, on_failure: "stop", max_retries: 0 }); + } catch (err) { + setError((err as Error).message); + } finally { + setImportingInline(false); + } }} /> > ); } - -function LibraryCard({ step, onAdd, onEdit }: { step: WorkflowStep; onAdd: () => void; onEdit: () => void }) { - return ( - { - e.dataTransfer.setData("text/plain", JSON.stringify({ kind: "lib", stepId: step.step_id })); - }} - onClick={onAdd} - className="group relative mb-2 cursor-grab rounded-lg border border-border bg-surface-2 p-2 text-left hover:border-signal/50" - > - - ⠿ - - {step.source === "default" && } - {step.name} - e.stopPropagation()} - className="ml-auto hidden text-text-secondary hover:text-text-primary group-hover:block" - title="Export step" - > - ⬇ - - { - e.stopPropagation(); - onEdit(); - }} - className="hidden text-text-secondary hover:text-text-primary group-hover:block" - title="Edit step" - > - ✎ - - - {step.description && {step.description}} - - ); -}
{step.description}