From 23b403711d835fde1f8b6af658d9149360657452 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 29 Jun 2026 16:07:44 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20vollst=C3=A4ndige=20Verdrahtung=20erzwi?= =?UTF-8?q?ngen=20und=20unvollst=C3=A4ndige=20Elemente=20markieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/main/frontend/node-editor/node-editor.js | 38 +++++++++++ .../themes/tasklist-editor/styles.css | 6 ++ .../service/TaskListExporter.java | 68 +++++++++++-------- .../tasklisteditor/view/MainView.java | 10 +-- tsconfig.json | 2 +- 5 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/main/frontend/node-editor/node-editor.js b/src/main/frontend/node-editor/node-editor.js index 2957196..4a3f2f8 100644 --- a/src/main/frontend/node-editor/node-editor.js +++ b/src/main/frontend/node-editor/node-editor.js @@ -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 => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' @@ -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); diff --git a/src/main/frontend/themes/tasklist-editor/styles.css b/src/main/frontend/themes/tasklist-editor/styles.css index 6933995..ca9f42a 100644 --- a/src/main/frontend/themes/tasklist-editor/styles.css +++ b/src/main/frontend/themes/tasklist-editor/styles.css @@ -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; diff --git a/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java b/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java index 059527e..3f91c8a 100644 --- a/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java +++ b/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java @@ -86,53 +86,63 @@ public class TaskListExporter { * Meldung, falls die Bedingung verletzt ist */ private void validateWorkflow(JsonNode data, Map instances) { - List startNodes = new ArrayList<>(); - List endNodes = new ArrayList<>(); - for (Map.Entry 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 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 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 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 determineOrder(JsonNode data) { diff --git a/src/main/java/de/assecutor/tasklisteditor/view/MainView.java b/src/main/java/de/assecutor/tasklisteditor/view/MainView.java index a482679..002dfa1 100644 --- a/src/main/java/de/assecutor/tasklisteditor/view/MainView.java +++ b/src/main/java/de/assecutor/tasklisteditor/view/MainView.java @@ -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()); diff --git a/tsconfig.json b/tsconfig.json index e508169..f855f65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"],