import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { LuArrowLeft, LuCopy, LuPause, LuPlay, LuSave } from "react-icons/lu"; import { errorMessage } from "../api/client.js"; import { useCreateWorkflow, useSaveWorkflow, useSetWorkflowEnabled, useWorkflow, useWorkflowRevision, } from "../api/hooks.js"; import { DuplicateWorkflowDialog } from "../components/DuplicateWorkflowDialog.jsx"; import { NewWorkflowPresetDialog, shouldSkipNewWorkflowPreset } from "../components/NewWorkflowPresetDialog.jsx"; import { WorkflowFileIcon } from "../components/WorkflowFileIcon.jsx"; import { WorkflowVisualEditor } from "../components/workflow/WorkflowVisualEditor.jsx"; import { WorkflowHistoryPanel } from "../components/workflow/WorkflowHistoryPanel.jsx"; import { SaveWorkflowWarningsDialog, isSaveWarningsError, saveErrorMessage, saveWarningsFromError, } from "../components/workflow/SaveWorkflowWarningsDialog.jsx"; import { ConfirmDialog } from "../components/ConfirmDialog.jsx"; import { WorkflowRevisionBanner } from "../components/workflow/WorkflowRevisionBanner.jsx"; import { NEW_WORKFLOW_YAML, parseWorkflowYaml } from "../lib/workflow-doc.js"; import { DEFAULT_OWNER } from "../lib/tenant.js"; import { useNotifications } from "../notifications.jsx"; function WorkflowEditorLayout({ title, backTo = "/workflows", savePending, saveDisabled, saveError, onSave, onTest, onDuplicate, onToggleEnabled, enabled, enablePending, enableDisabled, enableError, children, }) { return (

{title}

{onToggleEnabled ? ( ) : null} {onTest ? ( ) : null} {onDuplicate ? ( ) : null}
{children} {saveError ?

{saveError}

: null} {enableError ?

{enableError}

: null}
); } export function WorkflowNewPage() { const navigate = useNavigate(); const { notify } = useNotifications(); const [ready, setReady] = useState(() => shouldSkipNewWorkflowPreset()); const [content, setContent] = useState(NEW_WORKFLOW_YAML); const [savedYaml, setSavedYaml] = useState(NEW_WORKFLOW_YAML); const [saveWarnings, setSaveWarnings] = useState(null); const create = useCreateWorkflow(); function onSave(saveAnyway = false) { create.mutate( { owner: DEFAULT_OWNER, content, saveAnyway }, { onSuccess: (data) => { setSaveWarnings(null); notify.success(`Created ${data.file}`); navigate( `/workflows/${encodeURIComponent(data.owner)}/${encodeURIComponent(data.file)}/edit`, ); }, onError: (err) => { if (isSaveWarningsError(err)) { setSaveWarnings(saveWarningsFromError(err)); return; } notify.error(errorMessage(err)); }, }, ); } if (!ready) { return ( navigate("/workflows")} onChoose={(yaml) => { const initial = yaml == null || yaml === "" ? NEW_WORKFLOW_YAML : yaml; setContent(initial); setSavedYaml(initial); setReady(true); }} /> ); } return ( <> onSave(false)} savePending={create.isPending} saveError={create.isError && !saveWarnings ? errorMessage(create.error) : null} >
{saveWarnings ? ( setSaveWarnings(null)} onSaveAnyway={() => onSave(true)} /> ) : null} ); } export function WorkflowEditPage() { const navigate = useNavigate(); const { notify } = useNotifications(); const { owner: rawOwner, file: rawFile } = useParams(); const owner = decodeURIComponent(rawOwner ?? ""); const file = decodeURIComponent(rawFile ?? ""); const existing = useWorkflow(owner, file); const save = useSaveWorkflow(); const setEnabled = useSetWorkflowEnabled(); const [content, setContent] = useState(""); const [savedYaml, setSavedYaml] = useState(""); const [contentReady, setContentReady] = useState(false); const [testOpen, setTestOpen] = useState(false); const [duplicateOpen, setDuplicateOpen] = useState(false); const [saveWarnings, setSaveWarnings] = useState(null); const [previewRevision, setPreviewRevision] = useState(null); const [previewMeta, setPreviewMeta] = useState(null); const [previewBaseline, setPreviewBaseline] = useState(null); const [discardConfirm, setDiscardConfirm] = useState(null); const [savePreviewConfirm, setSavePreviewConfirm] = useState(false); const routeKey = `${owner}/${file}`; const [activeKey, setActiveKey] = useState(routeKey); const previewQuery = useWorkflowRevision(owner, file, previewRevision); if (activeKey !== routeKey) { setActiveKey(routeKey); setContent(""); setSavedYaml(""); setContentReady(false); setTestOpen(false); setDuplicateOpen(false); setPreviewRevision(null); setPreviewMeta(null); setPreviewBaseline(null); setDiscardConfirm(null); setSavePreviewConfirm(false); } useEffect(() => { if (existing.isLoading) return; if (previewRevision != null) return; if (!contentReady && existing.data?.content != null) { setContent(existing.data.content); setSavedYaml(existing.data.content); setContentReady(true); } }, [existing.data, existing.isLoading, contentReady, previewRevision]); useEffect(() => { if (previewRevision == null || previewQuery.isLoading) return; if (previewQuery.data?.content != null) { setContent(previewQuery.data.content); setPreviewBaseline(previewQuery.data.content); setPreviewMeta({ created_at: previewQuery.data.created_at }); } }, [previewRevision, previewQuery.data, previewQuery.isLoading]); function isDirty() { const baseline = previewRevision != null ? previewBaseline : savedYaml; return contentReady && baseline != null && content !== baseline; } function exitPreview() { setPreviewRevision(null); setPreviewMeta(null); setPreviewBaseline(null); existing.refetch().then((result) => { const next = result.data?.content; if (next != null) { setContent(next); setSavedYaml(next); } }); } function applySelectRevision(revision, meta) { if (revision == null) { exitPreview(); return; } setPreviewRevision(revision); if (meta?.created_at) { setPreviewMeta({ created_at: meta.created_at }); } } function onSelectRevision(revision, meta) { if (isDirty()) { setDiscardConfirm(() => () => applySelectRevision(revision, meta)); return; } applySelectRevision(revision, meta); } function onBackToCurrent() { if (isDirty()) { setDiscardConfirm(() => () => exitPreview()); return; } exitPreview(); } function onReverted() { setPreviewRevision(null); setPreviewMeta(null); setPreviewBaseline(null); existing.refetch().then((result) => { const next = result.data?.content; if (next != null) { setContent(next); setSavedYaml(next); } }); } function onSave(saveAnyway = false) { save.mutate( { owner, file, content, saveAnyway }, { onSuccess: () => { setSavedYaml(content); setSaveWarnings(null); setSavePreviewConfirm(false); notify.success("Workflow saved"); if (previewRevision != null) { setPreviewRevision(null); setPreviewMeta(null); setPreviewBaseline(null); } }, onError: (err) => { if (isSaveWarningsError(err)) { setSaveWarnings(saveWarningsFromError(err)); return; } }, }, ); } function onSaveRequest() { const liveYaml = existing.data?.content; if (previewRevision != null && content !== liveYaml) { setSavePreviewConfirm(true); return; } onSave(false); } if (existing.isLoading) { return (
); } if (existing.isError) { return (
Back

Workflow not found

); } const parsedDoc = parseWorkflowYaml(content); const yamlOk = !parsedDoc.parseError; const workflowName = parsedDoc.doc?.name?.trim(); const pageTitle = workflowName ? `${workflowName}` : file; const isPreviewing = previewRevision != null; const previewLoading = isPreviewing && previewQuery.isLoading; return ( <> {pageTitle} {file} } savePending={save.isPending} saveDisabled={!contentReady || previewLoading} saveError={save.isError && !saveWarnings ? saveErrorMessage(save.error) : null} onSave={() => onSaveRequest()} onTest={isPreviewing ? undefined : () => setTestOpen(true)} onDuplicate={isPreviewing ? undefined : () => setDuplicateOpen(true)} onToggleEnabled={ isPreviewing ? undefined : () => setEnabled.mutate({ owner, file, enabled: existing.data?.parsed?.enabled === false, }) } enabled={existing.data?.parsed?.enabled !== false} enablePending={setEnabled.isPending} enableDisabled={!contentReady || Boolean(existing.data?.parseError) || !yamlOk} enableError={setEnabled.isError ? errorMessage(setEnabled.error) : null} >
{isPreviewing && previewMeta ? ( ) : null} {previewLoading ? (
) : ( setTestOpen(false)} /> )}
{duplicateOpen ? ( setDuplicateOpen(false)} onDuplicated={(data) => { setDuplicateOpen(false); navigate( `/workflows/${encodeURIComponent(data.owner)}/${encodeURIComponent(data.file)}/edit`, ); }} /> ) : null} {saveWarnings ? ( setSaveWarnings(null)} onSaveAnyway={() => onSave(true)} /> ) : null} {savePreviewConfirm ? ( setSavePreviewConfirm(false)} onConfirm={() => onSave(false)} /> ) : null} {discardConfirm ? ( setDiscardConfirm(null)} onConfirm={() => { const action = discardConfirm; setDiscardConfirm(null); action?.(); }} /> ) : null} ); }