feat: implement Phase 5 workflow frontend features

- Template Gallery: category-filtered grid in WorkflowsSection with
  template cards showing name, description, category/source badges,
  and node/agent counts with 'Use Template' action
- Export/Import: dropdown export (YAML/Mermaid/DOT) and import
  (YAML/JSON) modals in WorkflowEditor header toolbar via new
  WorkflowExportImportPanel component
- Pattern-to-Workflow upgrade: ArrowUpRight icon button on each
  pattern row in PatternsSection calling upgradePatternToWorkflow
- Canvas polish: zoom in/out/fit-view controls panel at bottom-right
  of WorkflowGraphCanvas, zoom percentage display, Ctrl+A select-all
  keyboard shortcut, and selectionOnDrag enabled

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-05 21:28:43 +02:00
co-authored by Copilot
parent 4971dcf9fc
commit 55df0c3b61
5 changed files with 486 additions and 5 deletions
+61 -1
View File
@@ -1,5 +1,5 @@
import { Fragment, useCallback, useMemo, useState } from 'react';
import { AlertCircle, CheckCircle, ChevronLeft, ChevronRight, Info, Plus, Trash2, X } from 'lucide-react';
import { AlertCircle, CheckCircle, ChevronLeft, ChevronRight, Download, Info, Plus, Trash2, Upload, X } from 'lucide-react';
import type { ModelDefinition } from '@shared/domain/models';
import type {
@@ -18,6 +18,7 @@ import { createId } from '@shared/utils/ids';
import { ToggleSwitch } from '@renderer/components/ui';
import { WorkflowGraphCanvas } from './workflow/WorkflowGraphCanvas';
import { ExportDropdown, ExportModal, ImportModal } from './workflow/WorkflowExportImportPanel';
import { WorkflowGraphInspector } from './workflow/WorkflowGraphInspector';
import { WorkflowNodePalette } from './workflow/WorkflowNodePalette';
@@ -29,6 +30,8 @@ interface WorkflowEditorProps {
onDelete?: () => void;
onSave: () => void;
onBack: () => void;
onExportWorkflow?: (format: 'yaml' | 'mermaid' | 'dot') => Promise<{ content: string }>;
onImportWorkflow?: (content: string, format: 'yaml' | 'json') => Promise<WorkflowDefinition>;
}
interface WorkflowBreadcrumb {
@@ -158,10 +161,15 @@ export function WorkflowEditor({
onDelete,
onSave,
onBack,
onExportWorkflow,
onImportWorkflow,
}: WorkflowEditorProps) {
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
const [breadcrumbs, setBreadcrumbs] = useState<WorkflowBreadcrumb[]>([]);
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [exportResult, setExportResult] = useState<{ format: 'yaml' | 'mermaid' | 'dot'; content: string } | null>(null);
const [showImportModal, setShowImportModal] = useState(false);
/* ── Active workflow resolution ──────────────────────────── */
@@ -371,6 +379,39 @@ export function WorkflowEditor({
</div>
</div>
<div className="no-drag flex items-center gap-2">
{onExportWorkflow && (
<div className="relative">
<button
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)]"
onClick={() => setShowExportDropdown((prev) => !prev)}
type="button"
aria-expanded={showExportDropdown}
aria-haspopup="listbox"
>
<Download className="size-3.5" />
Export
</button>
{showExportDropdown && (
<ExportDropdown
onSelectFormat={async (format) => {
const result = await onExportWorkflow(format);
setExportResult({ format, content: result.content });
}}
onClose={() => setShowExportDropdown(false)}
/>
)}
</div>
)}
{onImportWorkflow && (
<button
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)]"
onClick={() => setShowImportModal(true)}
type="button"
>
<Upload className="size-3.5" />
Import
</button>
)}
{onDelete && (
<button
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] text-[var(--color-status-error)] transition-all duration-200 hover:bg-[var(--color-status-error)]/10"
@@ -509,6 +550,25 @@ export function WorkflowEditor({
/>
</div>
</div>
{exportResult && (
<ExportModal
format={exportResult.format}
content={exportResult.content}
onClose={() => setExportResult(null)}
/>
)}
{showImportModal && onImportWorkflow && (
<ImportModal
onImport={async (content, format) => {
const imported = await onImportWorkflow(content, format);
onChange(imported);
return imported;
}}
onClose={() => setShowImportModal(false)}
/>
)}
</div>
);
}