diff --git a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx index d0e6861..fc7197c 100644 --- a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx +++ b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx @@ -85,8 +85,25 @@ const VisualiserTab: React.FC = ({ const predecessors = task.config.predecessors as string[] | undefined; if (!predecessors || predecessors.length === 0) { - columnByGuid.set(guid, 0); - return 0; + // Spread root tasks across columns when there are multiple starts + const rootTasks = tasks.filter((t) => { + const preds = t.config.predecessors as string[] | undefined; + return !preds || preds.length === 0; + }); + + rootTasks.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 rootIndex = rootTasks.findIndex( + (t) => (t.config.guid as string) === guid, + ); + + const column = Math.max(0, rootIndex); + columnByGuid.set(guid, column); + return column; } // If multiple predecessors, position task at the average of their columns @@ -241,6 +258,13 @@ const VisualiserTab: React.FC = ({ return found?.positions ?? []; }; + const getColumnPercent = (column: number): number => { + const numColumns = columnRange.max - columnRange.min + 1; + if (numColumns <= 1) return 50; + const gridCol = column - columnRange.min; + return ((gridCol + 0.5) / numColumns) * 100; + }; + // Calculate connector positions based on grid columns const getConnectorPositionsByColumn = ( levelTasks: CreateWorkflowTemplateVersion["tasks"][], @@ -275,6 +299,7 @@ const VisualiserTab: React.FC = ({ const getPredecessorPositionsByColumn = ( levelTasks: CreateWorkflowTemplateVersion["tasks"][], + excludeTasks?: Set, ): number[] => { const numColumns = columnRange.max - columnRange.min + 1; if (numColumns <= 1) return [50]; @@ -284,6 +309,9 @@ const VisualiserTab: React.FC = ({ levelTasks.forEach((task) => { const preds = (task.config.predecessors as string[]) ?? []; preds.forEach((predGuid) => { + // Skip excluded tasks (e.g., those with override) + if (excludeTasks && excludeTasks.has(predGuid)) return; + const predColumn = taskColumnMap.current.get(predGuid); if (predColumn !== undefined) { predColumns.add(predColumn); @@ -291,10 +319,9 @@ const VisualiserTab: React.FC = ({ }); }); - return Array.from(predColumns).map((predColumn) => { - const gridCol = predColumn - columnRange.min; - return ((gridCol + 0.5) / numColumns) * 100; - }); + return Array.from(predColumns).map((predColumn) => + getColumnPercent(predColumn), + ); }; const renderConnector = ( @@ -402,7 +429,7 @@ const VisualiserTab: React.FC = ({ ); } - // Otherwise render SVG with line from start to end (always, even if vertical) + // Render orthogonal connector (vertical-horizontal-vertical) return (
@@ -585,7 +641,7 @@ const VisualiserTab: React.FC = ({ let styleObj: React.CSSProperties = {}; - if (isRootTask && numColumns > 1) { + if (isRootTask && numColumns > 1 && rootTaskCount === 1) { // Root tasks span all columns and center their content styleObj = { gridColumn: `1 / -1`, @@ -644,12 +700,89 @@ const VisualiserTab: React.FC = ({
{index < levels.length - 1 - ? renderConnector( - levels[index + 1].length, - getConnectorPositionsByColumn(levels[index + 1]), - getPredecessorPositionsByColumn(levels[index + 1]), - levels[index + 1], - ) + ? (() => { + const currentLevel = levels[index]; + const nextLevel = levels[index + 1]; + + // Tasks with override should not have normal progression connectors + const overrideTasks = new Set( + currentLevel + .filter( + (task) => + task.config.overrideDefaultTaskProgression === true, + ) + .map((task) => task.config.guid as string), + ); + + const hasMergeTask = nextLevel.some((task) => { + const preds = + (task.config.predecessors as string[]) ?? []; + // Only consider predecessors without override + const validPreds = preds.filter( + (predGuid) => !overrideTasks.has(predGuid), + ); + if (validPreds.length <= 1) return false; + + const predColumns = validPreds + .map((predGuid) => taskColumnMap.current.get(predGuid)) + .filter((col) => col !== undefined) as number[]; + + return new Set(predColumns).size > 1; + }); + + if (hasMergeTask) { + // Filter to tasks with at least one valid predecessor + const tasksWithValidPreds = nextLevel.filter((task) => { + const preds = + (task.config.predecessors as string[]) ?? []; + return preds.some( + (predGuid) => !overrideTasks.has(predGuid), + ); + }); + + return renderConnector( + tasksWithValidPreds.length, + getConnectorPositionsByColumn(tasksWithValidPreds), + getPredecessorPositionsByColumn( + tasksWithValidPreds, + overrideTasks, + ), + tasksWithValidPreds, + ); + } + + const groups = new Map< + string, + CreateWorkflowTemplateVersion["tasks"][] + >(); + + nextLevel.forEach((task) => { + const preds = + (task.config.predecessors as string[]) ?? []; + preds.forEach((predGuid) => { + // Skip predecessors with override + if (overrideTasks.has(predGuid)) return; + + const list = groups.get(predGuid) ?? []; + list.push(task); + groups.set(predGuid, list); + }); + }); + + return Array.from(groups.entries()).map( + ([predGuid, children]) => { + const predColumn = taskColumnMap.current.get(predGuid); + if (predColumn === undefined) return null; + + return renderConnector( + children.length, + getConnectorPositionsByColumn(children), + [getColumnPercent(predColumn)], + children, + ); + }, + ); + })() : null} ))}