Basic flowchart without branching is working.

This commit is contained in:
Colin Dawson 2026-02-25 22:06:01 +00:00
parent 03a5e5bcea
commit 5fb218908d
4 changed files with 195 additions and 3 deletions

View File

@ -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 {

109
src/Sass/visualiser.scss Normal file
View File

@ -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);
}
}

View File

@ -194,7 +194,7 @@ const WorkflowTemplateDetails: React.FC<{ editMode: boolean }> = ({
</Tab>,
<Tab key="visualFlow" id="visualFlow" label={t("Visual Flow")}>
<VisualiserTab data={data} />
<VisualiserTab data={data} taskValidation={taskValidation} />
</Tab>,
];

View File

@ -1,13 +1,95 @@
import React from "react";
import { CreateWorkflowTemplateVersion } from "../services/WorkflowTemplateService";
import ValidationErrorIcon from "../../../../components/validationErrorIcon";
interface VisualiserTabProps {
data: CreateWorkflowTemplateVersion;
taskValidation: Record<string, boolean>;
}
const VisualiserTab: React.FC<VisualiserTabProps> = ({ data }) => {
const VisualiserTab: React.FC<VisualiserTabProps> = ({
data,
taskValidation,
}) => {
const tasks = data.tasks;
const orderedTasks: CreateWorkflowTemplateVersion["tasks"] = [];
return <div>Visualiser</div>;
if (tasks.length > 0) {
const byGuid = new Map<string, CreateWorkflowTemplateVersion["tasks"][0]>();
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<string>();
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 (
<div className="visualiser-root">
<div className="visualiser-flow">
{nodes.map((node, index) => (
<React.Fragment key={node.key}>
<div className="visualiser-node">
<div className="visualiser-node-content">
{node.isTask && (
<ValidationErrorIcon
visible={taskValidation[node.guid as string] === false}
/>
)}
<span>{node.label}</span>
</div>
</div>
{index < nodes.length - 1 && (
<div aria-hidden="true" className="visualiser-connector">
<div className="visualiser-connector-line" />
<div className="visualiser-connector-arrow" />
<div className="visualiser-connector-tail" />
</div>
)}
</React.Fragment>
))}
</div>
</div>
);
};
export default VisualiserTab;