Issue #1 + #3: Validierung zeigt keine Details / Fehler-Lokalisierung NEU: ValidationPanel Component - Zeigt alle Fehler und Warnungen mit vollständigen Details - Gruppiert nach Severity (Fehler/Warnungen) - Aufklappbar pro Gruppe (collapsible) - Click-to-Jump: Fehler mit nodeId sind klickbar - Klick selektiert betroffene Node → Config-Panel öffnet sich - Schließbar, öffnet sich automatisch bei neuen Fehler/Warnungen Features: - Fixed position (bottom-right, über Canvas) - Farb-Kodierung: Rot (Errors) / Gelb (Warnings) - Hover-Effekt auf klickbare Items - Type-Labels (structure, isolation, etc.) - Scrollbar bei vielen Fehler/Warnungen - X-Button zum Schließen UX-Verbesserungen: - Kein Raten mehr: Zeigt WAS der Fehler ist - Kein Suchen mehr: Klick springt direkt zur Node - Übersichtlich auch bei vielen Fehler/Warnungen - Funktioniert in großen Workflows Technisch: - handleValidationNodeClick: setSelectedNodeId - Auto-show bei errors.length > 0 || warnings.length > 0 - State: showValidationPanel (closable) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
8.6 KiB
JavaScript
247 lines
8.6 KiB
JavaScript
import { useState } from 'react'
|
|
import { AlertCircle, AlertTriangle, ChevronDown, ChevronRight, X } from 'lucide-react'
|
|
|
|
/**
|
|
* ValidationPanel - Zeigt Fehler und Warnungen mit Details
|
|
*
|
|
* Features:
|
|
* - Aufklappbar (collapsible)
|
|
* - Click-to-Jump zu betroffener Node
|
|
* - Gruppierung nach Severity
|
|
* - Klare visuelle Trennung
|
|
*/
|
|
export function ValidationPanel({ errors, warnings, onNodeClick, onClose }) {
|
|
const [isExpanded, setIsExpanded] = useState(true)
|
|
const [showErrors, setShowErrors] = useState(true)
|
|
const [showWarnings, setShowWarnings] = useState(true)
|
|
|
|
const totalCount = errors.length + warnings.length
|
|
|
|
if (totalCount === 0) return null
|
|
|
|
const handleItemClick = (item) => {
|
|
if (item.nodeId && onNodeClick) {
|
|
onNodeClick(item.nodeId)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div style={{
|
|
position: 'fixed',
|
|
bottom: 90,
|
|
right: 20,
|
|
width: 400,
|
|
maxHeight: '60vh',
|
|
background: 'var(--surface)',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: 8,
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
zIndex: 100
|
|
}}>
|
|
{/* Header */}
|
|
<div
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
style={{
|
|
padding: '12px 16px',
|
|
borderBottom: isExpanded ? '1px solid var(--border)' : 'none',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
background: errors.length > 0 ? 'rgba(216, 90, 48, 0.1)' : 'rgba(255, 193, 7, 0.1)'
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
|
<AlertCircle size={18} color={errors.length > 0 ? 'var(--danger)' : '#f59e0b'} />
|
|
<span style={{ fontWeight: 600, fontSize: 14 }}>
|
|
Validierung
|
|
</span>
|
|
<span style={{ fontSize: 12, color: 'var(--text3)' }}>
|
|
{errors.length > 0 && `${errors.length} Fehler`}
|
|
{errors.length > 0 && warnings.length > 0 && ', '}
|
|
{warnings.length > 0 && `${warnings.length} Warnung${warnings.length > 1 ? 'en' : ''}`}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onClose && onClose()
|
|
}}
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
padding: 4,
|
|
display: 'flex',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<X size={16} color="var(--text3)" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{isExpanded && (
|
|
<div style={{
|
|
overflowY: 'auto',
|
|
maxHeight: 'calc(60vh - 60px)'
|
|
}}>
|
|
{/* Errors */}
|
|
{errors.length > 0 && (
|
|
<div>
|
|
<div
|
|
onClick={() => setShowErrors(!showErrors)}
|
|
style={{
|
|
padding: '8px 16px',
|
|
background: 'rgba(216, 90, 48, 0.05)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
borderBottom: '1px solid var(--border)'
|
|
}}
|
|
>
|
|
{showErrors ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
<AlertCircle size={16} color="var(--danger)" />
|
|
<span style={{ fontWeight: 600, fontSize: 13, color: 'var(--danger)' }}>
|
|
Fehler ({errors.length})
|
|
</span>
|
|
</div>
|
|
{showErrors && (
|
|
<div>
|
|
{errors.map((error, idx) => (
|
|
<div
|
|
key={idx}
|
|
onClick={() => handleItemClick(error)}
|
|
style={{
|
|
padding: '10px 16px',
|
|
borderBottom: idx < errors.length - 1 ? '1px solid var(--border)' : 'none',
|
|
cursor: error.nodeId ? 'pointer' : 'default',
|
|
background: error.nodeId ? 'transparent' : 'transparent',
|
|
transition: 'background 0.15s',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (error.nodeId) e.currentTarget.style.background = 'rgba(216, 90, 48, 0.05)'
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'transparent'
|
|
}}
|
|
>
|
|
<div style={{
|
|
fontSize: 13,
|
|
color: 'var(--text1)',
|
|
marginBottom: 4
|
|
}}>
|
|
{error.message}
|
|
</div>
|
|
{error.nodeId && (
|
|
<div style={{
|
|
fontSize: 11,
|
|
color: 'var(--text3)',
|
|
fontFamily: 'monospace'
|
|
}}>
|
|
→ Klicken um zu Node zu springen
|
|
</div>
|
|
)}
|
|
{error.type && (
|
|
<div style={{
|
|
fontSize: 10,
|
|
color: 'var(--text3)',
|
|
marginTop: 4,
|
|
textTransform: 'uppercase'
|
|
}}>
|
|
{error.type}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Warnings */}
|
|
{warnings.length > 0 && (
|
|
<div>
|
|
<div
|
|
onClick={() => setShowWarnings(!showWarnings)}
|
|
style={{
|
|
padding: '8px 16px',
|
|
background: 'rgba(255, 193, 7, 0.05)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
borderBottom: '1px solid var(--border)'
|
|
}}
|
|
>
|
|
{showWarnings ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
<AlertTriangle size={16} color="#f59e0b" />
|
|
<span style={{ fontWeight: 600, fontSize: 13, color: '#f59e0b' }}>
|
|
Warnungen ({warnings.length})
|
|
</span>
|
|
</div>
|
|
{showWarnings && (
|
|
<div>
|
|
{warnings.map((warning, idx) => (
|
|
<div
|
|
key={idx}
|
|
onClick={() => handleItemClick(warning)}
|
|
style={{
|
|
padding: '10px 16px',
|
|
borderBottom: idx < warnings.length - 1 ? '1px solid var(--border)' : 'none',
|
|
cursor: warning.nodeId ? 'pointer' : 'default',
|
|
background: 'transparent',
|
|
transition: 'background 0.15s',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (warning.nodeId) e.currentTarget.style.background = 'rgba(255, 193, 7, 0.05)'
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'transparent'
|
|
}}
|
|
>
|
|
<div style={{
|
|
fontSize: 13,
|
|
color: 'var(--text1)',
|
|
marginBottom: 4
|
|
}}>
|
|
{warning.message}
|
|
</div>
|
|
{warning.nodeId && (
|
|
<div style={{
|
|
fontSize: 11,
|
|
color: 'var(--text3)',
|
|
fontFamily: 'monospace'
|
|
}}>
|
|
→ Klicken um zu Node zu springen
|
|
</div>
|
|
)}
|
|
{warning.type && (
|
|
<div style={{
|
|
fontSize: 10,
|
|
color: 'var(--text3)',
|
|
marginTop: 4,
|
|
textTransform: 'uppercase'
|
|
}}>
|
|
{warning.type}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|