feat: vollständige Verdrahtung erzwingen und unvollständige Elemente markieren

- Workflow-Validierung verschärft: jedes Element muss alle vorhandenen Ein- und Ausgänge verbunden haben (Start nur Ausgang, Abschluss nur Eingang)
- Unvollständig verdrahtete Elemente werden im Editor live hellrot markiert
- Fix: tsconfig ignoreDeprecations zurück auf "5.0" (TS 5.7.3 lehnt "6.0" ab -> Vite-Build brach ab)
- MainView: Beschriftungen auf "Workflow" angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 16:07:44 +02:00
parent cf9c73635b
commit 23b403711d
5 changed files with 89 additions and 35 deletions

View File

@@ -194,6 +194,34 @@ if (!window.__taskListEditorDragInit) {
}, true);
}
/**
* Markiert alle Knoten, deren Ein- oder Ausgänge nicht vollständig verdrahtet
* sind, mit der CSS-Klasse {@code node-incomplete} (hellroter Hintergrund).
* Start-Knoten (kein Eingang) und Abschluss-Knoten (kein Ausgang) werden anhand
* ihrer tatsächlich vorhandenen Ports bewertet.
*/
function refreshValidity(state) {
if (!state) return;
const editor = state.editor;
const data = editor.drawflow.drawflow[editor.module].data;
for (const id in data) {
const node = data[id];
const incomplete = hasOpenPort(node.inputs) || hasOpenPort(node.outputs);
const el = state.container.querySelector('#node-' + id);
if (el) el.classList.toggle('node-incomplete', incomplete);
}
}
/** Liefert true, wenn mindestens einer der vorhandenen Ports keine Verbindung hat. */
function hasOpenPort(ports) {
if (!ports) return false;
for (const key in ports) {
const connections = ports[key] && ports[key].connections;
if (!connections || connections.length === 0) return true;
}
return false;
}
function buildNodeHtml(label, color) {
const safe = String(label).replace(/[<>&"']/g, c => ({
'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;'
@@ -325,6 +353,15 @@ function attach(host) {
}
});
// Unvollständig verdrahtete Elemente (offener Ein- oder Ausgang) hellrot
// markieren. Nach jeder strukturellen Änderung neu bewerten.
const refresh = () => requestAnimationFrame(() => refreshValidity(state));
editor.on('connectionCreated', refresh);
editor.on('connectionRemoved', refresh);
editor.on('nodeCreated', refresh);
editor.on('nodeRemoved', refresh);
editor.on('import', refresh);
// Klick in die Nähe einer Verbindung (auf leerem Canvas-Hintergrund) wählt
// diese aus robuster als das exakte Treffen der dünnen Linie. Läuft in der
// Capture-Phase vor Drawflow, um das Verschieben/Deselektieren zu verhindern.
@@ -543,6 +580,7 @@ window.taskListEditor = {
editor.removeNodeOutput(nodeId, `output_${current}`);
current--;
}
requestAnimationFrame(() => refreshValidity(state));
},
updateNodeLabel(host, nodeId, label) {
const state = editors.get(host);

View File

@@ -261,6 +261,12 @@ body, .tasklist-app {
flex-direction: column;
}
/* Unvollständig verdrahtetes Element (offener Ein- oder Ausgang): hellrot. */
.drawflow-node.node-incomplete .task-node {
background: #ffdede;
border-color: #ef4444;
}
.task-node-gear {
position: absolute;
top: 12px;

View File

@@ -86,53 +86,63 @@ public class TaskListExporter {
* Meldung, falls die Bedingung verletzt ist
*/
private void validateWorkflow(JsonNode data, Map<Integer, BlockInstance> instances) {
List<Integer> startNodes = new ArrayList<>();
List<Integer> endNodes = new ArrayList<>();
for (Map.Entry<Integer, BlockInstance> entry : instances.entrySet()) {
int taskType = entry.getValue().type().taskType();
// Es muss mindestens ein Start- und ein Abschluss-Element geben.
boolean hasStart = false;
boolean hasEnd = false;
for (BlockInstance instance : instances.values()) {
int taskType = instance.type().taskType();
if (taskType == BlockTypeRegistry.START_TASK_TYPE) {
startNodes.add(entry.getKey());
hasStart = true;
} else if (taskType == BlockTypeRegistry.END_TASK_TYPE) {
endNodes.add(entry.getKey());
hasEnd = true;
}
}
if (startNodes.isEmpty()) {
if (!hasStart) {
throw new IllegalStateException("Es muss ein Start-Element vorhanden sein.");
}
if (endNodes.isEmpty()) {
if (!hasEnd) {
throw new IllegalStateException("Es muss ein Abschluss-Element vorhanden sein.");
}
for (int id : startNodes) {
if (!hasConnectedOutput(data, id)) {
throw new IllegalStateException("Das Start-Element muss mit dem Ablauf verbunden sein.");
}
}
for (int id : endNodes) {
if (!hasConnectedInput(data, id)) {
throw new IllegalStateException("Das Abschluss-Element muss mit dem Ablauf verbunden sein.");
// Jedes Element muss vollständig verdrahtet sein: jeder vorhandene Ein-
// und Ausgang muss mit mindestens einer Linie belegt sein. Start besitzt
// nur einen Ausgang, Abschluss nur einen Eingang nicht vorhandene Ports
// werden dabei nicht gefordert.
List<String> nodeIds = new ArrayList<>();
data.fieldNames().forEachRemaining(nodeIds::add);
for (String nodeId : nodeIds) {
JsonNode node = data.path(nodeId);
if (!allPortsConnected(node.path("inputs")) || !allPortsConnected(node.path("outputs"))) {
throw new IllegalStateException(
"Jedes Element muss vollständig verbunden sein "
+ nodeLabel(instances, nodeId)
+ "“ hat einen offenen Ein- oder Ausgang.");
}
}
}
private boolean hasConnectedOutput(JsonNode data, int nodeId) {
JsonNode outputs = data.path(String.valueOf(nodeId)).path("outputs");
for (JsonNode output : outputs) {
if (output.path("connections").size() > 0) {
return true;
}
}
/** Prüft, ob alle vorhandenen Ports (Ein- oder Ausgänge) mindestens eine Verbindung haben. */
private boolean allPortsConnected(JsonNode ports) {
for (JsonNode port : ports) {
if (port.path("connections").size() == 0) {
return false;
}
}
return true;
}
private boolean hasConnectedInput(JsonNode data, int nodeId) {
JsonNode inputs = data.path(String.valueOf(nodeId)).path("inputs");
for (JsonNode input : inputs) {
if (input.path("connections").size() > 0) {
return true;
/** Liefert einen sprechenden Namen eines Knotens für Fehlermeldungen. */
private String nodeLabel(Map<Integer, BlockInstance> instances, String nodeId) {
try {
BlockInstance instance = instances.get(Integer.parseInt(nodeId));
if (instance != null) {
String topic = instance.topic();
return (topic == null || topic.isBlank()) ? instance.type().label() : topic;
}
} catch (NumberFormatException ignored) {
// Fällt auf die Knoten-ID zurück.
}
return false;
return "#" + nodeId;
}
private List<Integer> determineOrder(JsonNode data) {

View File

@@ -61,7 +61,7 @@ import java.util.Map;
import java.util.Set;
@Route("")
@PageTitle("TaskList Editor")
@PageTitle("Workflow Editor")
public class MainView extends VerticalLayout implements BeforeEnterObserver {
/** Query-Parameter des Deep-Links, der das Übergabe-Token enthält. */
@@ -143,13 +143,13 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
header.setPadding(true);
header.setSpacing(true);
H2 title = new H2("TaskList Editor");
H2 title = new H2("Workflow Editor");
title.addClassName("tasklist-title");
Span subtitle = new Span("Funktionsblöcke per Drag & Drop in eine Reihenfolge bringen");
subtitle.addClassName("tasklist-subtitle");
Button clear = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
Button clear = new Button("Workflow löschen", new Icon(VaadinIcon.TRASH));
clear.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
clear.addClickListener(e -> {
nodeEditor.clear();
@@ -157,11 +157,11 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
Notification.show("Canvas geleert");
});
Button saveCanvas = new Button("Canvas speichern", new Icon(VaadinIcon.HARDDRIVE));
Button saveCanvas = new Button("Workflow speichern", new Icon(VaadinIcon.HARDDRIVE));
saveCanvas.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
saveCanvas.addClickListener(e -> saveCanvas());
Button loadCanvas = new Button("Canvas laden", new Icon(VaadinIcon.FOLDER_OPEN));
Button loadCanvas = new Button("Worklow laden", new Icon(VaadinIcon.FOLDER_OPEN));
loadCanvas.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
loadCanvas.addClickListener(e -> openLoadCanvasDialog());

View File

@@ -22,7 +22,7 @@
"noUnusedParameters": false,
"experimentalDecorators": true,
"useDefineForClassFields": false,
"ignoreDeprecations": "6.0",
"ignoreDeprecations": "5.0",
"baseUrl": "src/main/frontend",
"paths": {
"@vaadin/flow-frontend": ["generated/jar-resources"],