From 5fb218908dd48f9512de13819bb5297a51d95b62 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Wed, 25 Feb 2026 22:06:01 +0000 Subject: [PATCH] Basic flowchart without branching is working. --- src/Sass/global.scss | 1 + src/Sass/visualiser.scss | 109 ++++++++++++++++++ .../WorkflowTemplateDetails.tsx | 2 +- .../components/VisualisetTab.tsx | 86 +++++++++++++- 4 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 src/Sass/visualiser.scss diff --git a/src/Sass/global.scss b/src/Sass/global.scss index 3ebf7ec..ae179f0 100644 --- a/src/Sass/global.scss +++ b/src/Sass/global.scss @@ -34,6 +34,7 @@ $color-mode-type: media-query; // or "class" if you prefer manual control @import "./addTaskButton.scss"; @import "./selectableList.scss"; @import "./checklist.scss"; +@import "./visualiser.scss"; //Changes needed to make MS Edge behave the same as other browsers input::-ms-reveal { diff --git a/src/Sass/visualiser.scss b/src/Sass/visualiser.scss new file mode 100644 index 0000000..ba9bf92 --- /dev/null +++ b/src/Sass/visualiser.scss @@ -0,0 +1,109 @@ +.visualiser-root { + display: flex; + justify-content: center; + padding: $spacePadding; +} + +.visualiser-flow { + width: 320px; + background: linear-gradient( + 180deg, + rgba($blue, 0.12), + rgba($mode--light-bg, 0.9) + ); + border: 1px solid rgba($blue, 0.25); + border-radius: 16px; + padding: 20px 16px; + box-shadow: 0 10px 24px rgba($leftMenu-background, 0.15); + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.visualiser-node { + width: 220px; + padding: 12px 14px; + border-radius: 12px; + background: linear-gradient( + 135deg, + lighten($mode--light-bg, 2%), + lighten($blue, 38%) + ); + border: 1px solid rgba($blue, 0.28); + color: $leftMenu-background; + font-weight: 600; + text-align: center; + letter-spacing: 0.2px; + box-shadow: 0 6px 14px rgba($blue, 0.2); +} + +.visualiser-node-content { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.visualiser-connector { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + margin: 2px 0; +} + +.visualiser-connector-line { + width: 2px; + height: 18px; + background: linear-gradient(180deg, rgba($blue, 0.6), rgba($blue, 0.2)); +} + +.visualiser-connector-arrow { + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 8px solid rgba($blue, 0.6); +} + +.visualiser-connector-tail { + width: 2px; + height: 10px; + background: rgba($blue, 0.2); +} + +@include color-mode(dark) { + .visualiser-flow { + background: linear-gradient( + 180deg, + rgba($blue, 0.18), + rgba($mode--dark-bg, 0.95) + ); + border: 1px solid rgba($blue, 0.35); + box-shadow: 0 12px 28px rgba($leftMenu-background, 0.35); + } + + .visualiser-node { + background: linear-gradient( + 135deg, + lighten($mode--dark-bg, 6%), + rgba($blue, 0.35) + ); + border: 1px solid rgba($blue, 0.45); + color: $leftMenu-color; + box-shadow: 0 8px 18px rgba($blue, 0.28); + } + + .visualiser-connector-line { + background: linear-gradient(180deg, rgba($blue, 0.8), rgba($blue, 0.3)); + } + + .visualiser-connector-arrow { + border-top-color: rgba($blue, 0.8); + } + + .visualiser-connector-tail { + background: rgba($blue, 0.3); + } +} diff --git a/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx b/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx index 59c1f77..3f78066 100644 --- a/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx +++ b/src/modules/manager/workflowTemplates/WorkflowTemplateDetails.tsx @@ -194,7 +194,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({ , - + , ]; diff --git a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx index 5d3f5b6..c84b98f 100644 --- a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx +++ b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx @@ -1,13 +1,95 @@ +import React from "react"; import { CreateWorkflowTemplateVersion } from "../services/WorkflowTemplateService"; +import ValidationErrorIcon from "../../../../components/validationErrorIcon"; interface VisualiserTabProps { data: CreateWorkflowTemplateVersion; + taskValidation: Record; } -const VisualiserTab: React.FC = ({ data }) => { +const VisualiserTab: React.FC = ({ + data, + taskValidation, +}) => { const tasks = data.tasks; + const orderedTasks: CreateWorkflowTemplateVersion["tasks"] = []; - return
Visualiser
; + if (tasks.length > 0) { + const byGuid = new Map(); + tasks.forEach((task) => { + byGuid.set(task.config.guid as string, task); + }); + + const startTask = tasks.find( + (task) => + !task.config.predecessors || + (task.config.predecessors as string[]).length === 0, + ); + + if (startTask) { + const visited = new Set(); + let current: CreateWorkflowTemplateVersion["tasks"][0] | undefined = + startTask; + + while (current && !visited.has(current.config.guid as string)) { + orderedTasks.push(current); + visited.add(current.config.guid as string); + + const currentGuid = current.config.guid as string; + const nextTask = tasks.find((task) => { + const predecessors = task.config.predecessors as string[] | undefined; + return predecessors?.length === 1 && predecessors[0] === currentGuid; + }); + + current = nextTask; + } + } + + tasks.forEach((task) => { + if (!orderedTasks.includes(task)) { + orderedTasks.push(task); + } + }); + } + + const nodes = [ + { key: "start", label: "Start", isTask: false }, + ...orderedTasks.map((task) => ({ + key: task.config.guid as string, + guid: task.config.guid as string, + label: (task.config.name as string) || task.type, + isTask: true, + })), + { key: "end", label: "End", isTask: false }, + ]; + + return ( +
+
+ {nodes.map((node, index) => ( + +
+
+ {node.isTask && ( + + )} + {node.label} +
+
+ {index < nodes.length - 1 && ( +