/** * CanvasHeader - Workflow controls, version selector, and execute result. */ import React, { useState, useRef, useEffect, useMemo } from 'react'; import { FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaBookmark, FaCaretDown, FaSave, FaPlus, FaChevronLeft, FaChevronRight, } from 'react-icons/fa'; import { HiOutlineMagnifyingGlassMinus, HiOutlineMagnifyingGlassPlus, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineTrash, HiOutlineDocumentDuplicate, HiOutlineArrowLongRight, HiOutlineChatBubbleLeftEllipsis, HiOutlineSquares2X2, } from 'react-icons/hi2'; import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi'; import styles from './Automation2FlowEditor.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; import { getUserDataCache } from '../../../utils/userCache'; import { Button } from '../../UiComponents/Button'; const ZOOM_PRESET_PERCENTS = [25, 50, 75, 100, 125, 150, 200, 400] as const; export interface CanvasHeaderCanvasEditProps { zoomPercent: number; selectedNodeCount: number; connectionSelected: boolean; stickyNoteSelected: boolean; connectionToolActive: boolean; canUndo: boolean; canRedo: boolean; onZoomIn: () => void; onZoomOut: () => void; onZoomPercentCommit: (percent: number) => void; onFitWindow: () => void; onResetView: () => void; onUndo: () => void; onRedo: () => void; onDeleteSelection: () => void; onDuplicateNode: () => void; onToggleConnectionTool: () => void; /** Textnotiz auf die Canvas legen (ohne Workflow-Daten). */ onAddCanvasComment: () => void; /** Verschachtelte Rasterpfade (4.1 / 4.2 …); Haftnotizen unberührt. */ onArrangeNodes: () => void; } interface CanvasHeaderProps { workflows: Automation2Workflow[]; currentWorkflowId: string | null; onWorkflowSelect: (workflowId: string | null) => void; onNew: () => void; onSave: () => void; onExecute: () => void; onToggleWorkspacePanel?: () => void; workspacePanelOpen?: boolean; saving: boolean; executing: boolean; hasNodes: boolean; /** When set, required-field graph errors block a normal run; message is the * run button tooltip. Click still fires `onExecuteBlockedClick` to focus * the first offending node. */ executeBlockedReason?: string | null; onExecuteBlockedClick?: () => void; executeResult: ExecuteGraphResponse | null; versions?: AutoVersion[]; currentVersionId?: string | null; onVersionSelect?: (versionId: string | null) => void; onPublishVersion?: (versionId: string) => void; onUnpublishVersion?: (versionId: string) => void; onArchiveVersion?: (versionId: string) => void; onCreateDraft?: () => void; versionLoading?: boolean; onSaveAsTemplate?: (scope: AutoTemplateScope) => void; templateSaving?: boolean; onNewFromTemplate?: () => void; /** Sysadmin-only: when true, NodeConfigPanel renders the static * "Schema (Typ-Referenz)" block and per-parameter type-badges. */ verboseSchema?: boolean; onVerboseSchemaChange?: (next: boolean) => void; canvasEdit?: CanvasHeaderCanvasEditProps; } function _getStatusBadge(t: (key: string) => string): Record { return { draft: { label: t('Entwurf'), color: 'var(--warning-color, #ffc107)' }, published: { label: t('Veröffentlicht'), color: 'var(--success-color, #28a745)' }, archived: { label: t('Archiviert'), color: 'var(--text-secondary, #666)' }, }; } const _tb = 'secondary' as const; const _ts = 'sm' as const; export const CanvasHeader: React.FC = ({ workflows, currentWorkflowId, onWorkflowSelect, onNew, onSave, onExecute, onToggleWorkspacePanel, workspacePanelOpen, saving, executing, hasNodes, executeBlockedReason, onExecuteBlockedClick, executeResult, versions, currentVersionId, onVersionSelect, onPublishVersion, onUnpublishVersion, onArchiveVersion, onCreateDraft, versionLoading, onSaveAsTemplate, templateSaving, onNewFromTemplate, verboseSchema, onVerboseSchemaChange, canvasEdit, }) => { const { t } = useLanguage(); const _isSysAdmin = getUserDataCache()?.isSysAdmin === true; const statusBadge = _getStatusBadge(t); const currentVersion = versions?.find((v) => v.id === currentVersionId); const currentStatus = currentVersion?.status || 'draft'; const badge = statusBadge[currentStatus] || statusBadge.draft; const [newMenuOpen, setNewMenuOpen] = useState(false); const newMenuRef = useRef(null); const [templateMenuOpen, setTemplateMenuOpen] = useState(false); const templateMenuRef = useRef(null); const [zoomMenuOpen, setZoomMenuOpen] = useState(false); const zoomMenuRef = useRef(null); const [zoomInputDraft, setZoomInputDraft] = useState(''); useEffect(() => { const zp = canvasEdit?.zoomPercent; if (zp !== undefined) setZoomInputDraft(String(zp)); }, [canvasEdit?.zoomPercent]); useEffect(() => { const _handleClickOutside = (e: MouseEvent) => { if (newMenuRef.current && !newMenuRef.current.contains(e.target as Node)) setNewMenuOpen(false); if (templateMenuRef.current && !templateMenuRef.current.contains(e.target as Node)) setTemplateMenuOpen(false); if (zoomMenuRef.current && !zoomMenuRef.current.contains(e.target as Node)) setZoomMenuOpen(false); }; document.addEventListener('mousedown', _handleClickOutside); return () => document.removeEventListener('mousedown', _handleClickOutside); }, []); const scopeLabels = useMemo( () => ({ user: t('Meine Vorlagen'), instance: t('Instanz'), mandate: t('Mandant'), }) as Record, [t] ); const _panelOpen = workspacePanelOpen ?? false; const _runAriaLabel = executing ? t('Ausführen…') : executeBlockedReason ? t('Pflicht-Felder fehlen') : t('Ausführen'); const _runTitle = executeBlockedReason ?? (hasNodes ? t('Ausführen') : t('Keine Nodes zum Ausführen.')); const _executeBannerSegmentClass = !executeResult ? '' : executeResult.success ? executeResult.warning ? styles.canvasHeaderExecuteBannerWarning : styles.canvasHeaderExecuteBannerSuccess : executeResult.paused ? styles.canvasHeaderExecuteBannerPaused : styles.canvasHeaderExecuteBannerError; const _commitZoomDraft = () => { if (!canvasEdit) return; const raw = zoomInputDraft.replace(/%/g, '').replace(',', '.').trim(); const n = parseFloat(raw); if (!Number.isFinite(n)) { setZoomInputDraft(String(canvasEdit.zoomPercent)); return; } canvasEdit.onZoomPercentCommit(Math.min(400, Math.max(25, Math.round(n)))); setZoomMenuOpen(false); }; const _canDeleteSelection = !!canvasEdit && (canvasEdit.selectedNodeCount > 0 || canvasEdit.connectionSelected || canvasEdit.stickyNoteSelected); const _singleNodeOnly = !!canvasEdit && canvasEdit.selectedNodeCount === 1 && !canvasEdit.connectionSelected; return (
{onToggleWorkspacePanel && (
{newMenuOpen && onNewFromTemplate && (
)}
{templateMenuOpen && (
{(['user', 'instance', 'mandate'] as const).map((s) => ( ))}
)} )} {_isSysAdmin && onVerboseSchemaChange && ( )} {canvasEdit && (
setZoomInputDraft(e.target.value)} onBlur={_commitZoomDraft} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); _commitZoomDraft(); } }} aria-label={t('Zoomstufe (Prozent)')} title={t('Zoomstufe (Prozent)')} /> %
{zoomMenuOpen && (
{ZOOM_PRESET_PERCENTS.map((pct) => ( ))}
)}
)} {currentWorkflowId && versions && versions.length > 0 && (
{t('Version:')} {badge.label} {currentVersion && currentStatus === 'draft' && onPublishVersion && ( )} {currentVersion && currentStatus === 'published' && onUnpublishVersion && ( )} {currentVersion && currentStatus !== 'archived' && onArchiveVersion && ( )} {onCreateDraft && ( )} {versionLoading && }
)} {executeResult && (
{executeResult.success ? ( executeResult.warning ? ( <>{executeResult.warning} ) : ( <>{t('Ausführung abgeschlossen')} ) ) : executeResult.paused ? ( <> {t('Workflow pausiert. Öffne ')} {t('Workflows/Tasks')} {t(' in der Sidebar, um den Task zu bearbeiten.')} ) : ( <>{executeResult.error ?? t('Unbekannter Fehler')} )}
)} ); };