import React, { useRef, useState, useEffect } from "react"; import { CreateWorkflowTemplateVersion } from "../services/WorkflowTemplateService"; import ValidationErrorIcon from "../../../../components/validationErrorIcon"; interface VisualiserTabProps { data: CreateWorkflowTemplateVersion; taskValidation: Record; } interface ArrowPositions { levelIndex: number; positions: number[]; } const VisualiserTab: React.FC = ({ data, taskValidation, }) => { const tasks = data.tasks; const levels: CreateWorkflowTemplateVersion["tasks"][] = []; const levelRowRefs = useRef<(HTMLDivElement | null)[]>([]); const [arrowPositions, setArrowPositions] = useState([]); const taskColumnMap = useRef>(new Map()); const visualiserContainerRef = useRef(null); const [taskDOMPositions, setTaskDOMPositions] = useState< Map >(new Map()); // Calculate column range directly from taskColumnMap in render const allColumns = Array.from(taskColumnMap.current.values()); const columnRange = { min: allColumns.length > 0 ? Math.min(...allColumns) : 0, max: allColumns.length > 0 ? Math.max(...allColumns) : 0, }; if (tasks.length > 0) { const byGuid = new Map(); tasks.forEach((task) => { byGuid.set(task.config.guid as string, task); }); const levelByGuid = new Map(); const columnByGuid = new Map(); const getLevel = (task: CreateWorkflowTemplateVersion["tasks"][0]) => { const guid = task.config.guid as string; const cached = levelByGuid.get(guid); if (cached !== undefined) { return cached; } const predecessors = task.config.predecessors as string[] | undefined; if (!predecessors || predecessors.length === 0) { levelByGuid.set(guid, 1); return 1; } const predecessorLevels = predecessors.map((pred) => { const predTask = byGuid.get(pred); if (!predTask) { return 1; } return getLevel(predTask); }); const nextLevel = Math.max(...predecessorLevels, 0) + 1; levelByGuid.set(guid, nextLevel); return nextLevel; }; // First pass: calculate levels for all tasks tasks.forEach((task) => { getLevel(task); }); // Second pass: assign columns based on predecessor-child relationships const getColumn = ( task: CreateWorkflowTemplateVersion["tasks"][0], ): number => { const guid = task.config.guid as string; const cached = columnByGuid.get(guid); if (cached !== undefined) { return cached; } const predecessors = task.config.predecessors as string[] | undefined; if (!predecessors || predecessors.length === 0) { columnByGuid.set(guid, 0); return 0; } // Get predecessor column and find which child index this is const predTask = byGuid.get(predecessors[0]); if (!predTask) { columnByGuid.set(guid, 0); return 0; } const predColumn = getColumn(predTask); // Find all siblings (tasks with same predecessor) const siblings = tasks.filter((t) => { const preds = t.config.predecessors as string[] | undefined; return preds && preds[0] === predecessors[0]; }); // Sort siblings by name for consistent ordering siblings.sort((a, b) => { const nameA = (a.config.name as string) || a.type || ""; const nameB = (b.config.name as string) || b.type || ""; return nameA.localeCompare(nameB); }); const siblingIndex = siblings.findIndex( (s) => (s.config.guid as string) === guid, ); // Assign columns: spread siblings around parent column const startCol = Math.max( 0, predColumn - Math.floor(siblings.length / 2), ); const column = startCol + siblingIndex; columnByGuid.set(guid, column); return column; }; // Calculate columns for all tasks tasks.forEach((task) => { const col = getColumn(task); taskColumnMap.current.set(task.config.guid as string, col); }); const levelsMap = new Map(); tasks.forEach((task) => { const level = getLevel(task); const list = levelsMap.get(level) ?? []; list.push(task); levelsMap.set(level, list); }); // Sort tasks within each level by column [...levelsMap.entries()] .sort((a, b) => a[0] - b[0]) .forEach((entry) => { entry[1].sort((a, b) => { const colA = taskColumnMap.current.get(a.config.guid as string) ?? 0; const colB = taskColumnMap.current.get(b.config.guid as string) ?? 0; return colA - colB; }); levels.push(entry[1]); }); } useEffect(() => { // Measure actual positions of task nodes and calculate arrow positions const positions: ArrowPositions[] = []; levelRowRefs.current.forEach((levelRow, levelIndex) => { if (!levelRow) return; const levelRect = levelRow.getBoundingClientRect(); const nodes = levelRow.querySelectorAll(".visualiser-node"); const nodePositions: number[] = []; nodes.forEach((node) => { const nodeRect = node.getBoundingClientRect(); // Calculate center of node relative to level row const relativeCenter = (nodeRect.left - levelRect.left + nodeRect.width / 2) / levelRect.width; // Convert to SVG coordinates (0-100) nodePositions.push(relativeCenter * 100); }); if (nodePositions.length > 0) { positions.push({ levelIndex, positions: nodePositions }); } }); setArrowPositions(positions); }, [tasks]); useEffect(() => { // Measure task DOM positions for outcome connectors if (!visualiserContainerRef.current) return; const containerRect = visualiserContainerRef.current.getBoundingClientRect(); const positions = new Map(); const nodes = visualiserContainerRef.current.querySelectorAll(".visualiser-node"); nodes.forEach((node) => { const taskName = node.querySelector(".visualiser-node-content span"); if (!taskName) return; // Find task by name const task = tasks.find( (t) => (t.config.name as string) === taskName.textContent, ); if (!task) return; const nodeRect = node.getBoundingClientRect(); const relX = ((nodeRect.left - containerRect.left + nodeRect.width / 2) / containerRect.width) * 100; const relY = ((nodeRect.top - containerRect.top + nodeRect.height / 2) / containerRect.height) * 100; positions.set(task.config.guid as string, { x: relX, y: relY }); }); setTaskDOMPositions(positions); }, [tasks, levels]); const getArrowPositionsForLevel = (levelIndex: number): number[] => { const found = arrowPositions.find((ap) => ap.levelIndex === levelIndex); return found?.positions ?? []; }; // Calculate connector positions based on grid columns const getConnectorPositionsByColumn = ( levelTasks: CreateWorkflowTemplateVersion["tasks"][], ): number[] => { const numColumns = columnRange.max - columnRange.min + 1; if (numColumns <= 1) return [50]; return levelTasks.map((task) => { const taskColumn = taskColumnMap.current.get(task.config.guid as string) ?? 0; const gridCol = taskColumn - columnRange.min; // Map grid column to percentage (e.g., 2 columns: col 0 = 25%, col 1 = 75%) return ((gridCol + 0.5) / numColumns) * 100; }); }; const renderConnector = ( count: number, positions?: number[], predecessorPositions?: number[], ) => { if (count <= 1) { // For a single child, use predecessor position if available let startX = 50; let endX = 50; // If single child with predecessor positions, connect from the appropriate predecessor if (positions && positions.length === 1) { endX = positions[0]; if (predecessorPositions && predecessorPositions.length > 0) { // Find the closest predecessor position to the child position const closest = predecessorPositions.reduce((prev, curr) => Math.abs(curr - endX) < Math.abs(prev - endX) ? curr : prev, ); startX = closest; } } // If start and end are the same, use simple vertical connector if (!predecessorPositions || predecessorPositions.length === 0) { return (