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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user