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:
@@ -194,6 +194,34 @@ if (!window.__taskListEditorDragInit) {
|
|||||||
}, true);
|
}, 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) {
|
function buildNodeHtml(label, color) {
|
||||||
const safe = String(label).replace(/[<>&"']/g, c => ({
|
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
|
// 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
|
// 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.
|
// Capture-Phase vor Drawflow, um das Verschieben/Deselektieren zu verhindern.
|
||||||
@@ -543,6 +580,7 @@ window.taskListEditor = {
|
|||||||
editor.removeNodeOutput(nodeId, `output_${current}`);
|
editor.removeNodeOutput(nodeId, `output_${current}`);
|
||||||
current--;
|
current--;
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(() => refreshValidity(state));
|
||||||
},
|
},
|
||||||
updateNodeLabel(host, nodeId, label) {
|
updateNodeLabel(host, nodeId, label) {
|
||||||
const state = editors.get(host);
|
const state = editors.get(host);
|
||||||
|
|||||||
@@ -261,6 +261,12 @@ body, .tasklist-app {
|
|||||||
flex-direction: column;
|
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 {
|
.task-node-gear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|||||||
@@ -86,53 +86,63 @@ public class TaskListExporter {
|
|||||||
* Meldung, falls die Bedingung verletzt ist
|
* Meldung, falls die Bedingung verletzt ist
|
||||||
*/
|
*/
|
||||||
private void validateWorkflow(JsonNode data, Map<Integer, BlockInstance> instances) {
|
private void validateWorkflow(JsonNode data, Map<Integer, BlockInstance> instances) {
|
||||||
List<Integer> startNodes = new ArrayList<>();
|
// Es muss mindestens ein Start- und ein Abschluss-Element geben.
|
||||||
List<Integer> endNodes = new ArrayList<>();
|
boolean hasStart = false;
|
||||||
for (Map.Entry<Integer, BlockInstance> entry : instances.entrySet()) {
|
boolean hasEnd = false;
|
||||||
int taskType = entry.getValue().type().taskType();
|
for (BlockInstance instance : instances.values()) {
|
||||||
|
int taskType = instance.type().taskType();
|
||||||
if (taskType == BlockTypeRegistry.START_TASK_TYPE) {
|
if (taskType == BlockTypeRegistry.START_TASK_TYPE) {
|
||||||
startNodes.add(entry.getKey());
|
hasStart = true;
|
||||||
} else if (taskType == BlockTypeRegistry.END_TASK_TYPE) {
|
} else if (taskType == BlockTypeRegistry.END_TASK_TYPE) {
|
||||||
endNodes.add(entry.getKey());
|
hasEnd = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!hasStart) {
|
||||||
if (startNodes.isEmpty()) {
|
|
||||||
throw new IllegalStateException("Es muss ein Start-Element vorhanden sein.");
|
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.");
|
throw new IllegalStateException("Es muss ein Abschluss-Element vorhanden sein.");
|
||||||
}
|
}
|
||||||
for (int id : startNodes) {
|
|
||||||
if (!hasConnectedOutput(data, id)) {
|
// Jedes Element muss vollständig verdrahtet sein: jeder vorhandene Ein-
|
||||||
throw new IllegalStateException("Das Start-Element muss mit dem Ablauf verbunden sein.");
|
// 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.
|
||||||
for (int id : endNodes) {
|
List<String> nodeIds = new ArrayList<>();
|
||||||
if (!hasConnectedInput(data, id)) {
|
data.fieldNames().forEachRemaining(nodeIds::add);
|
||||||
throw new IllegalStateException("Das Abschluss-Element muss mit dem Ablauf verbunden sein.");
|
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) {
|
/** Prüft, ob alle vorhandenen Ports (Ein- oder Ausgänge) mindestens eine Verbindung haben. */
|
||||||
JsonNode outputs = data.path(String.valueOf(nodeId)).path("outputs");
|
private boolean allPortsConnected(JsonNode ports) {
|
||||||
for (JsonNode output : outputs) {
|
for (JsonNode port : ports) {
|
||||||
if (output.path("connections").size() > 0) {
|
if (port.path("connections").size() == 0) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasConnectedInput(JsonNode data, int nodeId) {
|
/** Liefert einen sprechenden Namen eines Knotens für Fehlermeldungen. */
|
||||||
JsonNode inputs = data.path(String.valueOf(nodeId)).path("inputs");
|
private String nodeLabel(Map<Integer, BlockInstance> instances, String nodeId) {
|
||||||
for (JsonNode input : inputs) {
|
try {
|
||||||
if (input.path("connections").size() > 0) {
|
BlockInstance instance = instances.get(Integer.parseInt(nodeId));
|
||||||
return true;
|
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) {
|
private List<Integer> determineOrder(JsonNode data) {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Route("")
|
@Route("")
|
||||||
@PageTitle("TaskList Editor")
|
@PageTitle("Workflow Editor")
|
||||||
public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||||
|
|
||||||
/** Query-Parameter des Deep-Links, der das Übergabe-Token enthält. */
|
/** 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.setPadding(true);
|
||||||
header.setSpacing(true);
|
header.setSpacing(true);
|
||||||
|
|
||||||
H2 title = new H2("TaskList Editor");
|
H2 title = new H2("Workflow Editor");
|
||||||
title.addClassName("tasklist-title");
|
title.addClassName("tasklist-title");
|
||||||
|
|
||||||
Span subtitle = new Span("Funktionsblöcke per Drag & Drop in eine Reihenfolge bringen");
|
Span subtitle = new Span("Funktionsblöcke per Drag & Drop in eine Reihenfolge bringen");
|
||||||
subtitle.addClassName("tasklist-subtitle");
|
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.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
clear.addClickListener(e -> {
|
clear.addClickListener(e -> {
|
||||||
nodeEditor.clear();
|
nodeEditor.clear();
|
||||||
@@ -157,11 +157,11 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
Notification.show("Canvas geleert");
|
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.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
saveCanvas.addClickListener(e -> saveCanvas());
|
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.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
loadCanvas.addClickListener(e -> openLoadCanvasDialog());
|
loadCanvas.addClickListener(e -> openLoadCanvasDialog());
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"ignoreDeprecations": "6.0",
|
"ignoreDeprecations": "5.0",
|
||||||
"baseUrl": "src/main/frontend",
|
"baseUrl": "src/main/frontend",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||||
|
|||||||
Reference in New Issue
Block a user