mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
/**
|
|
* Process Flow Modal
|
|
*
|
|
* Displays a Mermaid flowchart for a process in a centered modal popup.
|
|
*/
|
|
|
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
import { X, GitBranch, Copy, Focus, Layers, ZoomIn, ZoomOut } from 'lucide-react';
|
|
import mermaid from 'mermaid';
|
|
import { ProcessData, generateProcessMermaid } from '../lib/mermaid-generator';
|
|
|
|
interface ProcessFlowModalProps {
|
|
process: ProcessData | null;
|
|
onClose: () => void;
|
|
onFocusInGraph?: (nodeIds: string[], processId: string) => void;
|
|
isFullScreen?: boolean;
|
|
}
|
|
|
|
// Initialize mermaid with cyan/purple theme matching GitNexus
|
|
// Initialize mermaid with cyan/purple theme matching GitNexus
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
suppressErrorRendering: true, // Try to suppress if supported
|
|
maxTextSize: 900000, // Increase from default 50000 to handle large combined diagrams
|
|
theme: 'base',
|
|
themeVariables: {
|
|
primaryColor: '#1e293b', // node bg
|
|
primaryTextColor: '#f1f5f9',
|
|
primaryBorderColor: '#22d3ee',
|
|
lineColor: '#94a3b8',
|
|
secondaryColor: '#1e293b',
|
|
tertiaryColor: '#0f172a',
|
|
mainBkg: '#1e293b', // background
|
|
nodeBorder: '#22d3ee',
|
|
clusterBkg: '#1e293b',
|
|
clusterBorder: '#475569',
|
|
titleColor: '#f1f5f9',
|
|
edgeLabelBackground: '#0f172a',
|
|
},
|
|
flowchart: {
|
|
curve: 'basis',
|
|
padding: 50,
|
|
nodeSpacing: 120,
|
|
rankSpacing: 140,
|
|
htmlLabels: true,
|
|
},
|
|
});
|
|
|
|
// Suppress distinct syntax error overlay
|
|
mermaid.parseError = (err) => {
|
|
// Suppress visual error - we handle errors in the render try/catch
|
|
console.debug('Mermaid parse error (suppressed):', err);
|
|
};
|
|
|
|
export const ProcessFlowModal = ({ process, onClose, onFocusInGraph, isFullScreen = false }: ProcessFlowModalProps) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const diagramRef = useRef<HTMLDivElement>(null);
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Full process map gets higher default zoom (667%) and max zoom (3000%)
|
|
const defaultZoom = isFullScreen ? 6.67 : 1;
|
|
const maxZoom = isFullScreen ? 30 : 10;
|
|
|
|
const [zoom, setZoom] = useState(defaultZoom);
|
|
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
const [isPanning, setIsPanning] = useState(false);
|
|
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
|
|
|
|
// Reset zoom when switching between full screen and regular mode
|
|
useEffect(() => {
|
|
setZoom(defaultZoom);
|
|
setPan({ x: 0, y: 0 });
|
|
}, [isFullScreen, defaultZoom]);
|
|
|
|
// Handle zoom with scroll wheel
|
|
useEffect(() => {
|
|
const handleWheel = (e: WheelEvent) => {
|
|
e.preventDefault();
|
|
const delta = e.deltaY * -0.001;
|
|
setZoom(prev => Math.min(Math.max(0.1, prev + delta), maxZoom));
|
|
};
|
|
|
|
const container = scrollContainerRef.current;
|
|
if (container) {
|
|
container.addEventListener('wheel', handleWheel, { passive: false });
|
|
return () => container.removeEventListener('wheel', handleWheel);
|
|
}
|
|
}, [process, maxZoom]); // Re-attach when process or maxZoom changes
|
|
|
|
// Handle keyboard zoom
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === '+' || e.key === '=') {
|
|
setZoom(prev => Math.min(prev + 0.2, maxZoom));
|
|
} else if (e.key === '-' || e.key === '_') {
|
|
setZoom(prev => Math.max(prev - 0.2, 0.1));
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [maxZoom]);
|
|
|
|
// Zoom in/out handlers
|
|
const handleZoomIn = useCallback(() => {
|
|
setZoom(prev => Math.min(prev + 0.25, maxZoom));
|
|
}, [maxZoom]);
|
|
|
|
const handleZoomOut = useCallback(() => {
|
|
setZoom(prev => Math.max(prev - 0.25, 0.1));
|
|
}, []);
|
|
|
|
// Handle pan with mouse drag
|
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
setIsPanning(true);
|
|
setPanStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
|
|
}, [pan]);
|
|
|
|
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
if (!isPanning) return;
|
|
setPan({ x: e.clientX - panStart.x, y: e.clientY - panStart.y });
|
|
}, [isPanning, panStart]);
|
|
|
|
const handleMouseUp = useCallback(() => {
|
|
setIsPanning(false);
|
|
}, []);
|
|
|
|
const resetView = useCallback(() => {
|
|
setZoom(defaultZoom);
|
|
setPan({ x: 0, y: 0 });
|
|
}, [defaultZoom]);
|
|
|
|
// Render mermaid diagram
|
|
useEffect(() => {
|
|
if (!process || !diagramRef.current) return;
|
|
|
|
const renderDiagram = async () => {
|
|
try {
|
|
// Check if we have raw mermaid code (from AI chat) or need to generate it
|
|
const mermaidCode = (process as any).rawMermaid
|
|
? (process as any).rawMermaid
|
|
: generateProcessMermaid(process);
|
|
const id = `mermaid-${Date.now()}`;
|
|
|
|
// Clear previous content
|
|
diagramRef.current!.innerHTML = '';
|
|
|
|
const { svg } = await mermaid.render(id, mermaidCode);
|
|
diagramRef.current!.innerHTML = svg;
|
|
} catch (error) {
|
|
console.error('Mermaid render error:', error);
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const isSizeError = errorMessage.includes('Maximum') || errorMessage.includes('exceeded');
|
|
|
|
diagramRef.current!.innerHTML = `
|
|
<div class="text-center p-8">
|
|
<div class="text-red-400 text-sm font-medium mb-2">
|
|
${isSizeError ? '📊 Diagram Too Large' : '⚠️ Render Error'}
|
|
</div>
|
|
<div class="text-slate-400 text-xs max-w-md">
|
|
${isSizeError
|
|
? `This diagram has ${process.steps?.length || 0} steps and is too complex to render. Try viewing individual processes instead of "All Processes".`
|
|
: `Unable to render diagram. Steps: ${process.steps?.length || 0}`
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
renderDiagram();
|
|
}, [process]);
|
|
|
|
// Close on escape
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', handleEscape);
|
|
return () => window.removeEventListener('keydown', handleEscape);
|
|
}, [onClose]);
|
|
|
|
// Close on backdrop click
|
|
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
if (e.target === containerRef.current) {
|
|
onClose();
|
|
}
|
|
}, [onClose]);
|
|
|
|
// Copy mermaid code to clipboard
|
|
const handleCopyMermaid = useCallback(async () => {
|
|
if (!process) return;
|
|
const mermaidCode = generateProcessMermaid(process);
|
|
await navigator.clipboard.writeText(mermaidCode);
|
|
}, [process]);
|
|
|
|
// Focus in graph
|
|
const handleFocusInGraph = useCallback(() => {
|
|
if (!process || !onFocusInGraph) return;
|
|
const nodeIds = process.steps.map(s => s.id);
|
|
onFocusInGraph(nodeIds, process.id);
|
|
onClose();
|
|
}, [process, onFocusInGraph, onClose]);
|
|
|
|
if (!process) return null;
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 animate-fade-in"
|
|
onClick={handleBackdropClick}
|
|
>
|
|
{/* Glassmorphism Modal */}
|
|
<div className={`bg-slate-900/60 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl shadow-cyan-500/10 flex flex-col animate-scale-in overflow-hidden relative ${isFullScreen
|
|
? 'w-[98%] h-[95vh] max-w-none'
|
|
: 'w-[95%] max-w-5xl max-h-[90vh]'
|
|
}`}>
|
|
{/* Subtle gradient overlay for extra glass feel */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent pointer-events-none" />
|
|
|
|
{/* Header */}
|
|
<div className="px-6 py-5 border-b border-white/10 relative z-10">
|
|
<h2 className="text-lg font-semibold text-white">
|
|
Process: {process.label}
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Diagram */}
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className={`flex-1 p-8 flex items-center justify-center relative z-10 overflow-hidden ${isFullScreen ? 'min-h-[70vh]' : 'min-h-[400px]'}`}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
style={{ cursor: isPanning ? 'grabbing' : 'grab' }}
|
|
>
|
|
<div
|
|
ref={diagramRef}
|
|
className="[&_.edgePath_.path]:stroke-slate-400 [&_.edgePath_.path]:stroke-2 [&_.marker]:fill-slate-400 transition-transform origin-center w-fit h-fit"
|
|
style={{
|
|
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Footer Actions */}
|
|
<div className="flex items-center justify-center gap-3 px-6 py-4 border-t border-white/10 bg-slate-900/50 relative z-10">
|
|
{/* Zoom controls */}
|
|
<div className="flex items-center gap-1 bg-white/5 border border-white/10 rounded-lg p-1">
|
|
<button
|
|
onClick={handleZoomOut}
|
|
className="p-2 text-slate-300 hover:text-white hover:bg-white/10 rounded-md transition-all"
|
|
title="Zoom out (-)"
|
|
>
|
|
<ZoomOut className="w-4 h-4" />
|
|
</button>
|
|
<span className="px-2 text-xs text-slate-400 font-mono min-w-[3rem] text-center">
|
|
{Math.round(zoom * 100)}%
|
|
</span>
|
|
<button
|
|
onClick={handleZoomIn}
|
|
className="p-2 text-slate-300 hover:text-white hover:bg-white/10 rounded-md transition-all"
|
|
title="Zoom in (+)"
|
|
>
|
|
<ZoomIn className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={resetView}
|
|
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-slate-300 hover:text-white bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all"
|
|
title="Reset zoom and pan"
|
|
>
|
|
Reset View
|
|
</button>
|
|
{onFocusInGraph && (
|
|
<button
|
|
onClick={handleFocusInGraph}
|
|
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-slate-900 bg-cyan-400 hover:bg-cyan-300 rounded-lg transition-all shadow-lg shadow-cyan-500/20"
|
|
>
|
|
<Focus className="w-4 h-4" />
|
|
Toggle Focus
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleCopyMermaid}
|
|
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-white bg-purple-600 hover:bg-purple-500 rounded-lg transition-all shadow-lg shadow-purple-500/20"
|
|
>
|
|
<Copy className="w-4 h-4" />
|
|
Copy Mermaid
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
className="px-5 py-2.5 text-sm font-medium text-slate-300 hover:text-white bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|