+ ${typeof stage.published !== 'undefined' ? `
${stage.published === 1 ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED')}
` : ''}
+ ${stage.default ? `
${translate('COM_WORKFLOW_GRAPH_DEFAULT')}
` : ''}
+
`;
+ }
+ stageEl.innerHTML = newHTML;
+ stageContainer.appendChild(stageEl);
+ });
+
+ const edges = generateEdges(state.transitions, state.stages);
+ svg.querySelectorAll('g.edge-group').forEach(group => {
+ if (!edges.find(e => e.id === group.dataset.edgeId)) group.remove();
+ });
+
+ edges.forEach(edge => {
+ let group = svg.querySelector(`g.edge-group[data-edge-id="${edge.id}"]`);
+ if (!group) {
+ group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ group.setAttribute('class', 'edge-group');
+ group.dataset.edgeId = edge.id;
+
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ path.setAttribute('class', 'transition-path');
+ path.setAttribute('marker-end', 'url(#arrowhead)');
+
+ const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ foreignObject.setAttribute('width', '1');
+ foreignObject.setAttribute('height', '1');
+ foreignObject.style.overflow = 'visible';
+
+ const labelDiv = document.createElement('div');
+ labelDiv.className = 'transition-label-content';
+ labelDiv.addEventListener('click', e => {
+ e.stopPropagation();
+ state.highlightedEdge = state.highlightedEdge === edge.id ? null : edge.id;
+ renderGraph(modal);
+ });
+
+ foreignObject.appendChild(labelDiv);
+ group.appendChild(path);
+ group.appendChild(foreignObject);
+ svg.appendChild(group);
+ }
+
+ const path = group.querySelector('path');
+ const foreignObject = group.querySelector('foreignObject');
+ const labelDiv = foreignObject.querySelector('div');
+
+ path.setAttribute('d', edge.pathData);
+ path.classList.toggle('highlighted', state.highlightedEdge === edge.id);
+ // Update marker for existing path as well
+ let markerId = 'arrowhead';
+ if (edge.arrowDirection === 'up') markerId = 'arrowhead-up';
+ else if (edge.arrowDirection === 'down') markerId = 'arrowhead-down';
+ else if (edge.arrowDirection === 'left') markerId = 'arrowhead-left';
+ path.setAttribute('marker-end', `url(#${markerId})`);
+
+ labelDiv.textContent = edge.label;
+ labelDiv.classList.toggle('highlighted', state.highlightedEdge === edge.id);
+
+ requestAnimationFrame(() => {
+ // Use max-content for label width
+ labelDiv.style.width = 'max-content';
+ const measuredWidth = labelDiv.getBoundingClientRect().width;
+ foreignObject.setAttribute('width', measuredWidth);
+ foreignObject.setAttribute('height', '32');
+ // Center the label at the control point
+ let labelY = edge.labelPosition.y - 16;
+ if (edge.isBidirectional && typeof edge.fromId !== 'undefined' && typeof edge.toId !== 'undefined') {
+ labelY += (edge.fromId < edge.toId ? -18 : 18);
+ }
+ foreignObject.setAttribute('x', edge.labelPosition.x - measuredWidth / 2);
+ foreignObject.setAttribute('y', labelY);
+ });
+ });
+
+ // Apply transform to graph
+ graph.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
+
+ // Apply transforms to background pattern if it exists
+ const workflowGraph = modal.querySelector('#workflow-graph');
+ if (workflowGraph) {
+ // Create a dynamic radial gradient where both dot size and spacing scale with zoom
+ const dotSize = Math.max(0.5, Math.min(1, state.scale)) * 1; // Dot size scales but has limits
+ const spacing = 15 * state.scale; // Grid spacing scales with zoom
+ workflowGraph.style.backgroundImage = `radial-gradient(circle at 1px 1px, var(--wf-dot-color) ${dotSize}px, transparent ${dotSize}px)`;
+ workflowGraph.style.backgroundSize = `${spacing}px ${spacing}px`;
+ workflowGraph.style.backgroundPosition = `${state.panX}px ${state.panY}px`;
+ }
+ }
+
+ function handleNodeDrag(startEvent, draggedStage) {
+ if (draggedStage.id === 'From Any') return;
+ const stageElement = document.getElementById(`stage-${draggedStage.id}`);
+ state.isDraggingStage = true;
+ const dragStart = { x: startEvent.clientX, y: startEvent.clientY, stageX: draggedStage.position.x, stageY: draggedStage.position.y };
+ stageElement.classList.add('dragging');
+
+ const onMouseMove = moveEvent => {
+ const newX = dragStart.stageX + (moveEvent.clientX - dragStart.x) / state.scale;
+ const newY = dragStart.stageY + (moveEvent.clientY - dragStart.y) / state.scale;
+ const stageToUpdate = state.stages.find(s => s.id === draggedStage.id);
+ if (stageToUpdate) {
+ stageToUpdate.position.x = newX;
+ stageToUpdate.position.y = newY;
+ }
+ renderGraph(document.querySelector('#workflow-graph'));
+ };
+
+ const onMouseUp = () => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ stageElement.classList.remove('dragging');
+ state.isDraggingStage = false;
+ };
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ }
+
+ async function init(modal) {
+ const container = modal.querySelector('#workflow-graph');
+
+ if (!container || container.dataset.initialized) {
+ return;
+ }
+ container.dataset.initialized = 'true';
+
+ const workflowContainer = container.querySelector('#workflow-container');
+
+ const workflowId = parseInt(workflowContainer.dataset.workflowId, 10);
+
+ if (!workflowId) return showMessageInModal('COM_WORKFLOW_GRAPH_ERROR_INVALID_ID', 'error');
+
+ const graph = modal.querySelector('#graph');
+ const svg = modal.querySelector('#connections');
+
+ // Vue Flow style arrowhead markers
+ svg.innerHTML = `