feat: Listen-Elemente, virtuelle Start/Abschluss-Blöcke und Workflow-Validierung
- Neue Aufgabentypen: Text-/Zahlen-Liste (fields) und Checkbox-Liste (entries); Export/Import um fields und choices erweitert - Typ-spezifische Element-Einstellungen in MongoDB (ElementSettingsService/-Document, ElementCollectionProperties) - Virtuelle Start-/Abschluss-Elemente (nur zur Darstellung, nicht exportiert); keine Einstellungen; Export erzwingt vorhandenes und verdrahtetes Start- und Abschluss-Element - Node-Editor: nur eine Verbindung je Ausgang (Start des Drags unterbunden) - Script-Block korrekt aus dem App-JSON gefiltert (Typ 0) - Fixes: tsconfig ignoreDeprecations 6.0; JDT-Null-Analyse (Method-Referenzen zu Lambdas) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
.env.example
18
.env.example
@@ -19,6 +19,24 @@ MONGODB_CANVAS_COLLECTION=canvas_layouts
|
||||
# Collection, in der die benutzerdefinierten Typen gespeichert werden:
|
||||
MONGODB_CUSTOM_TYPE_COLLECTION=custom_types
|
||||
|
||||
# --- Collections je Element-Typ ---------------------------------------------
|
||||
# Beim Speichern eines Canvas werden die Einstellungen jedes platzierten
|
||||
# Elements zusätzlich je Typ in eine eigene Collection geschrieben.
|
||||
# (Benutzerdefinierte Typen nutzen die Collection ihres Basistyps.)
|
||||
MONGODB_ELEMENT_ARRIVAL_COLLECTION=element_arrival
|
||||
MONGODB_ELEMENT_SIGNATURE_COLLECTION=element_signature
|
||||
MONGODB_ELEMENT_PHOTOS_COLLECTION=element_photos
|
||||
MONGODB_ELEMENT_RECEIPT_GIVER_COLLECTION=element_receipt_giver
|
||||
MONGODB_ELEMENT_REMARK_COLLECTION=element_remark
|
||||
MONGODB_ELEMENT_COMMISSION_NUMBER_COLLECTION=element_commission_number
|
||||
MONGODB_ELEMENT_CHECKBOX_COLLECTION=element_checkbox
|
||||
MONGODB_ELEMENT_BARCODE_COLLECTION=element_barcode
|
||||
MONGODB_ELEMENT_STATUS_SELECTION_COLLECTION=element_status_selection
|
||||
MONGODB_ELEMENT_TEXT_LIST_COLLECTION=element_text_list
|
||||
MONGODB_ELEMENT_NUMBER_LIST_COLLECTION=element_number_list
|
||||
MONGODB_ELEMENT_CHECKBOX_LIST_COLLECTION=element_checkbox_list
|
||||
MONGODB_ELEMENT_SCRIPT_COLLECTION=element_script
|
||||
|
||||
# Script-Block (grafisches Logik-Element) ausblenden:
|
||||
# true = Script-Element wird im Editor nicht angezeigt
|
||||
# false = Script-Element ist verfügbar (Standard)
|
||||
|
||||
@@ -5,6 +5,11 @@ import 'drawflow/dist/drawflow.min.css';
|
||||
// senkrecht in den Eingang des Ziels eintritt, bevor sie abbiegt.
|
||||
const EXIT_STUB = 5;
|
||||
|
||||
// Greif-Offset (in Bildschirm-Pixeln) zwischen Mauszeiger und linker oberer
|
||||
// Ecke des gezogenen Palette-Elements. Wird beim Drop abgezogen, damit der
|
||||
// Knoten an seiner gezogenen Position liegt statt mit der Ecke an der Maus.
|
||||
let dragGrabOffset = { x: 0, y: 0 };
|
||||
|
||||
function verticalCurvature(sx, sy, ex, ey) {
|
||||
const dx = ex - sx;
|
||||
const minOffset = 40;
|
||||
@@ -183,6 +188,8 @@ if (!window.__taskListEditorDragInit) {
|
||||
if (target && e.dataTransfer) {
|
||||
e.dataTransfer.setData('application/x-blocktype', target.dataset.blocktype);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
const r = target.getBoundingClientRect();
|
||||
dragGrabOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@@ -251,8 +258,10 @@ function attach(host) {
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const zoom = editor.zoom;
|
||||
const px = (e.clientX - rect.left - editor.canvas_x) / zoom;
|
||||
const py = (e.clientY - rect.top - editor.canvas_y) / zoom;
|
||||
// Greif-Offset abziehen, damit der Knoten an seiner gezogenen Position
|
||||
// liegt (linke obere Ecke wie während des Ziehens, nicht an der Maus).
|
||||
const px = (e.clientX - rect.left - editor.canvas_x - dragGrabOffset.x) / zoom;
|
||||
const py = (e.clientY - rect.top - editor.canvas_y - dragGrabOffset.y) / zoom;
|
||||
|
||||
addNodeInternal(host, def, px, py);
|
||||
});
|
||||
@@ -282,6 +291,40 @@ function attach(host) {
|
||||
host.$server && host.$server.onNodeRemoved(parseInt(id, 10));
|
||||
});
|
||||
|
||||
// Pro Ausgang ist nur eine Verbindung erlaubt. Hat ein Ausgang bereits eine
|
||||
// Linie, wird Drawflows Verbindungs-Drag gar nicht erst gestartet: Das
|
||||
// mousedown/touchstart auf dem Ausgangs-Punkt wird in der Capture-Phase
|
||||
// abgefangen, bevor Drawflows eigener Handler ausgelöst wird.
|
||||
const blockSecondConnectionStart = e => {
|
||||
if (e.type === 'mousedown' && e.button !== 0) return;
|
||||
const target = e.target;
|
||||
if (!(target instanceof Element) || !target.classList.contains('output')) return;
|
||||
const nodeEl = target.closest('.drawflow-node');
|
||||
if (!nodeEl) return;
|
||||
const id = parseInt(nodeEl.id.replace('node-', ''), 10);
|
||||
const outputClass = Array.from(target.classList).find(c => c.startsWith('output_'));
|
||||
if (!outputClass) return;
|
||||
const node = editor.drawflow.drawflow[editor.module].data[id];
|
||||
const output = node && node.outputs[outputClass];
|
||||
if (output && output.connections.length >= 1) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
container.addEventListener('mousedown', blockSecondConnectionStart, true);
|
||||
container.addEventListener('touchstart', blockSecondConnectionStart, true);
|
||||
|
||||
// Sicherheitsnetz: Sollte auf anderem Weg dennoch eine zweite Verbindung am
|
||||
// selben Ausgang entstehen, wird die neu erstellte sofort wieder entfernt.
|
||||
editor.on('connectionCreated', conn => {
|
||||
const data = editor.drawflow.drawflow[editor.module].data;
|
||||
const node = data[conn.output_id];
|
||||
const output = node && node.outputs[conn.output_class];
|
||||
if (output && output.connections.length > 1) {
|
||||
editor.removeSingleConnection(conn.output_id, conn.input_id, conn.output_class, conn.input_class);
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
@@ -296,6 +339,64 @@ function attach(host) {
|
||||
selectConnection(state, path);
|
||||
}, true);
|
||||
|
||||
// Verschieben von Knoten mit der RECHTEN Maustaste. Drawflow zieht Knoten
|
||||
// nur mit links; das Ziehen mit rechts wird hier eigenständig umgesetzt.
|
||||
let rightDrag = null;
|
||||
let suppressContextMenu = false;
|
||||
container.addEventListener('mousedown', e => {
|
||||
if (e.button !== 2) return;
|
||||
const nodeEl = e.target instanceof Element ? e.target.closest('.drawflow-node') : null;
|
||||
if (!nodeEl) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Drawflows eigene (Links-)Drag-Logik nicht auslösen
|
||||
rightDrag = {
|
||||
nodeEl,
|
||||
id: nodeEl.id.replace('node-', ''),
|
||||
lastX: e.clientX,
|
||||
lastY: e.clientY
|
||||
};
|
||||
suppressContextMenu = true;
|
||||
}, true);
|
||||
|
||||
const onRightMove = e => {
|
||||
if (!rightDrag) return;
|
||||
const zoom = editor.zoom || 1;
|
||||
const newLeft = rightDrag.nodeEl.offsetLeft + (e.clientX - rightDrag.lastX) / zoom;
|
||||
const newTop = rightDrag.nodeEl.offsetTop + (e.clientY - rightDrag.lastY) / zoom;
|
||||
rightDrag.lastX = e.clientX;
|
||||
rightDrag.lastY = e.clientY;
|
||||
rightDrag.nodeEl.style.left = newLeft + 'px';
|
||||
rightDrag.nodeEl.style.top = newTop + 'px';
|
||||
const data = editor.drawflow.drawflow[editor.module].data[rightDrag.id];
|
||||
if (data) { data.pos_x = newLeft; data.pos_y = newTop; }
|
||||
editor.updateConnectionNodes('node-' + rightDrag.id);
|
||||
};
|
||||
const onRightUp = () => { rightDrag = null; };
|
||||
document.addEventListener('mousemove', onRightMove);
|
||||
document.addEventListener('mouseup', onRightUp);
|
||||
|
||||
// Browser-Kontextmenü nach einem Rechts-Ziehen auf einem Knoten unterdrücken.
|
||||
container.addEventListener('contextmenu', e => {
|
||||
if (suppressContextMenu) {
|
||||
e.preventDefault();
|
||||
suppressContextMenu = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Zoomen mit dem Mausrad (ohne Strg). Läuft in der Capture-Phase und stoppt
|
||||
// die Propagation, damit Drawflows eigenes Strg+Rad-Zoom nicht zusätzlich
|
||||
// auslöst.
|
||||
const onWheelZoom = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.deltaY > 0) {
|
||||
editor.zoom_out();
|
||||
} else if (e.deltaY < 0) {
|
||||
editor.zoom_in();
|
||||
}
|
||||
};
|
||||
container.addEventListener('wheel', onWheelZoom, { capture: true, passive: false });
|
||||
|
||||
// Eine angeklickte Verbindung erhält den Fokus auf dem Canvas-Container,
|
||||
// damit die Entf-/Backspace-Taste sie löschen kann.
|
||||
editor.on('connectionSelected', () => {
|
||||
@@ -317,7 +418,12 @@ function attach(host) {
|
||||
editor.removeConnection();
|
||||
};
|
||||
document.addEventListener('keydown', deleteSelectedConnection);
|
||||
state.cleanup = () => document.removeEventListener('keydown', deleteSelectedConnection);
|
||||
state.cleanup = () => {
|
||||
document.removeEventListener('keydown', deleteSelectedConnection);
|
||||
document.removeEventListener('mousemove', onRightMove);
|
||||
document.removeEventListener('mouseup', onRightUp);
|
||||
container.removeEventListener('wheel', onWheelZoom, { capture: true });
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -286,6 +286,12 @@ body, .tasklist-app {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Virtuelle Workflow-Markierungen (Start/Abschluss) haben keine Einstellungen. */
|
||||
.type-start .task-node-gear,
|
||||
.type-abschluss .task-node-gear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-node-header {
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
@@ -418,6 +424,13 @@ body, .tasklist-app {
|
||||
margin: 0 0 var(--lumo-space-s);
|
||||
}
|
||||
|
||||
.properties-fieldlist-label {
|
||||
color: var(--lumo-secondary-text-color);
|
||||
font-size: var(--lumo-font-size-s);
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--lumo-space-xs);
|
||||
}
|
||||
|
||||
/* --- Script-/Logik-Editor --- */
|
||||
.script-editor,
|
||||
.script-editor-container {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.assecutor.tasklisteditor.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Konfiguration der MongoDB-Collections, in denen die Einstellungen der
|
||||
* einzelnen Element-Typen abgelegt werden.
|
||||
*
|
||||
* <p>Die Collection-Namen werden aus {@code application.properties} gebunden,
|
||||
* deren Werte wiederum aus den {@code MONGODB_ELEMENT_*}-Variablen der
|
||||
* {@code .env} stammen:
|
||||
* <pre>
|
||||
* tasklist.element-collections[text-liste-erfassen]=${MONGODB_ELEMENT_TEXT_LISTE_COLLECTION:element_text_liste_erfassen}
|
||||
* </pre>
|
||||
*
|
||||
* <p>Für jeden (festen wie benutzerdefinierten) Typ muss eine Zuordnung
|
||||
* existieren; benutzerdefinierte Typen werden zuvor auf ihren statischen
|
||||
* Basistyp aufgelöst. Fehlt ein Eintrag, ist das ein Konfigurationsfehler.
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "tasklist")
|
||||
public class ElementCollectionProperties {
|
||||
|
||||
/** Typ-ID → Collection-Name. */
|
||||
private final Map<String, String> elementCollections = new HashMap<>();
|
||||
|
||||
public Map<String, String> getElementCollections() {
|
||||
return elementCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Collection-Namen für die angegebene Typ-ID.
|
||||
*
|
||||
* @throws IllegalStateException wenn für die Typ-ID keine Zuordnung
|
||||
* konfiguriert ist
|
||||
*/
|
||||
public String collectionFor(String typeId) {
|
||||
String configured = elementCollections.get(typeId);
|
||||
if (configured == null || configured.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"Keine Collection für Element-Typ '" + typeId + "' konfiguriert "
|
||||
+ "(tasklist.element-collections).");
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package de.assecutor.tasklisteditor.document;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Einstellungen eines einzelnen platzierten Elements (Block-Instanz) eines
|
||||
* Canvas. Wird beim Speichern eines Canvas je Element-Typ in eine eigene
|
||||
* MongoDB-Collection geschrieben (Collection-Name pro Typ konfigurierbar, siehe
|
||||
* {@code tasklist.element-collections} bzw. die {@code MONGODB_ELEMENT_*}-
|
||||
* Variablen in der {@code .env}).
|
||||
*
|
||||
* <p>Die Verknüpfung zum Canvas erfolgt über {@link #canvasId}; {@link #nodeId}
|
||||
* identifiziert den Knoten innerhalb des Canvas.
|
||||
*/
|
||||
public class ElementSettingsDocument {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
/** ID des zugehörigen {@link CanvasDocument}. */
|
||||
private String canvasId;
|
||||
|
||||
/** Anzeigename des Canvas zum Zeitpunkt des Speicherns. */
|
||||
private String canvasName;
|
||||
|
||||
/** Knoten-ID innerhalb des Canvas (Drawflow). */
|
||||
private int nodeId;
|
||||
|
||||
/** Typ-ID des Elements (z. B. {@code text-liste-erfassen} oder {@code custom-1000}). */
|
||||
private String typeId;
|
||||
|
||||
/**
|
||||
* Bei benutzerdefinierten Typen die Typ-ID des statischen Basistyps, in
|
||||
* dessen Collection das Dokument abgelegt wird; bei festen Typen {@code null}.
|
||||
*/
|
||||
private String baseTypeId;
|
||||
|
||||
/** Bezeichnung des Tasks (entspricht {@code topic}). */
|
||||
private String topic;
|
||||
|
||||
/** Erfasste Property-Werte des Elements. */
|
||||
private Map<String, Object> values;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
public ElementSettingsDocument() {
|
||||
}
|
||||
|
||||
public ElementSettingsDocument(String canvasId, String canvasName, int nodeId,
|
||||
String typeId, String baseTypeId, String topic,
|
||||
Map<String, Object> values, Instant createdAt) {
|
||||
this.canvasId = canvasId;
|
||||
this.canvasName = canvasName;
|
||||
this.nodeId = nodeId;
|
||||
this.typeId = typeId;
|
||||
this.baseTypeId = baseTypeId;
|
||||
this.topic = topic;
|
||||
this.values = values;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCanvasId() {
|
||||
return canvasId;
|
||||
}
|
||||
|
||||
public void setCanvasId(String canvasId) {
|
||||
this.canvasId = canvasId;
|
||||
}
|
||||
|
||||
public String getCanvasName() {
|
||||
return canvasName;
|
||||
}
|
||||
|
||||
public void setCanvasName(String canvasName) {
|
||||
this.canvasName = canvasName;
|
||||
}
|
||||
|
||||
public int getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
public void setNodeId(int nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public String getTypeId() {
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public void setTypeId(String typeId) {
|
||||
this.typeId = typeId;
|
||||
}
|
||||
|
||||
public String getBaseTypeId() {
|
||||
return baseTypeId;
|
||||
}
|
||||
|
||||
public void setBaseTypeId(String baseTypeId) {
|
||||
this.baseTypeId = baseTypeId;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public Map<String, Object> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public void setValues(Map<String, Object> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,22 @@ public record BlockType(
|
||||
int taskType,
|
||||
int inputs,
|
||||
int outputs,
|
||||
List<PropertyDefinition> properties
|
||||
List<PropertyDefinition> properties,
|
||||
/** Verweis auf den statischen Basistyp (nur bei benutzerdefinierten Typen, sonst {@code null}). */
|
||||
String baseTypeId
|
||||
) {
|
||||
/** Konstruktor für feste Standard-Typen ohne Basistyp-Verweis. */
|
||||
public BlockType(String id, String label, String description, String icon, String color,
|
||||
int taskType, int inputs, int outputs, List<PropertyDefinition> properties) {
|
||||
this(id, label, description, icon, color, taskType, inputs, outputs, properties, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rein virtuelle Blöcke (z. B. Start/Abschluss) dienen nur der
|
||||
* Workflow-Darstellung im Canvas und werden nicht in die Task-Liste
|
||||
* exportiert. Sie sind an einer negativen Typnummer erkennbar.
|
||||
*/
|
||||
public boolean isVirtual() {
|
||||
return taskType < 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,39 @@ public class BlockTypeRegistry {
|
||||
|
||||
private final Map<String, BlockType> types = new LinkedHashMap<>();
|
||||
|
||||
/** Typnummer der virtuellen Start-Markierung (negativ = nicht exportiert). */
|
||||
public static final int START_TASK_TYPE = -1;
|
||||
|
||||
/** Typnummer der virtuellen Abschluss-Markierung (negativ = nicht exportiert). */
|
||||
public static final int END_TASK_TYPE = -2;
|
||||
|
||||
public BlockTypeRegistry(@Value("${tasklist.hide-script-block:false}") boolean hideScriptBlock) {
|
||||
// Rein virtuelle Workflow-Markierungen: Start (nur Ausgang) und Abschluss
|
||||
// (nur Eingang). Sie dienen ausschließlich der Darstellung eines
|
||||
// vollständigen Ablaufs im Canvas und werden NICHT exportiert
|
||||
// (negative Typnummer, siehe BlockType#isVirtual).
|
||||
register(new BlockType(
|
||||
"start",
|
||||
"Start",
|
||||
"Virtuelle Markierung für den Beginn des Workflows (wird nicht exportiert).",
|
||||
"vaadin:play",
|
||||
"#16a34a",
|
||||
START_TASK_TYPE,
|
||||
0, 1,
|
||||
List.of()
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"abschluss",
|
||||
"Abschluss",
|
||||
"Virtuelle Markierung für das Ende des Workflows (wird nicht exportiert).",
|
||||
"vaadin:flag-checkered",
|
||||
"#b91c1c",
|
||||
END_TASK_TYPE,
|
||||
1, 0,
|
||||
List.of()
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"ankunft-melden",
|
||||
"Ankunft melden",
|
||||
@@ -73,12 +105,12 @@ public class BlockTypeRegistry {
|
||||
// Fasst die früheren App-Typen Quittungsgeber, Bemerkung und
|
||||
// Kommissionsnummer zu einem einzigen Texterfassungs-Task zusammen.
|
||||
register(new BlockType(
|
||||
"text-erfassen",
|
||||
"Text erfassen",
|
||||
"Erfasst einen Text per Eingabefeld (z. B. Name, Bemerkung oder Nummer).",
|
||||
"quittungsgeber-erfassen",
|
||||
"Quittungsgeber erfassen",
|
||||
"Erfasst den Quittungsgeber per Eingabefeld.",
|
||||
"vaadin:input",
|
||||
"#dc2626",
|
||||
4,
|
||||
7,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
||||
@@ -88,16 +120,30 @@ public class BlockTypeRegistry {
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"zahl-erfassen",
|
||||
"Zahl erfassen",
|
||||
"Erfasst einen numerischen Wert per Zahleneingabe.",
|
||||
"vaadin:abacus",
|
||||
"#0891b2",
|
||||
5,
|
||||
"bemerkung-erfassen",
|
||||
"Bemerkung erfassen",
|
||||
"Erfasst eine Bemerkung per Eingabefeld.",
|
||||
"vaadin:input",
|
||||
"#dc2626",
|
||||
8,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.number("min", "Mindestwert", 0),
|
||||
PropertyDefinition.number("max", "Maximalwert (0 = unbegrenzt)", 0),
|
||||
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||
)
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"kommissionsnummer-erfassen",
|
||||
"Kommissionsnummer erfassen",
|
||||
"Erfasst eine Kommissionsnummer per Eingabefeld.",
|
||||
"vaadin:abacus",
|
||||
"#0891b2",
|
||||
9,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||
)
|
||||
@@ -123,7 +169,7 @@ public class BlockTypeRegistry {
|
||||
"Erfasst eine Liste gescannter Barcodes. (In der Flutter-App deaktiviert.)",
|
||||
"vaadin:barcode",
|
||||
"#0d9488",
|
||||
7,
|
||||
5,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
@@ -139,7 +185,7 @@ public class BlockTypeRegistry {
|
||||
"Lässt einen Status aus einer Auswahlliste wählen.",
|
||||
"vaadin:list-select",
|
||||
"#ea580c",
|
||||
8,
|
||||
11,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.textArea("choices", "Statusoptionen (eine pro Zeile)"),
|
||||
@@ -148,6 +194,55 @@ public class BlockTypeRegistry {
|
||||
)
|
||||
));
|
||||
|
||||
// Listen-Tasks: erfassen mehrerer gleichartiger Werte. Die Anzahl wird
|
||||
// analog zu "Fotos aufnehmen" über min/max gesteuert. Diese Typen haben
|
||||
// (noch) keinen Handler in der stadtbote-App und liegen daher oberhalb
|
||||
// des dort genutzten Bereichs (1–11).
|
||||
register(new BlockType(
|
||||
"text-liste-erfassen",
|
||||
"Liste von Texten erfassen",
|
||||
"Erfasst mehrere Texteinträge über vordefinierte Eingabefelder.",
|
||||
"vaadin:lines-list",
|
||||
"#4338ca",
|
||||
12,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.fieldList("fields", "Textfelder (Titel + Platzhalter)"),
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||
)
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"zahl-liste-erfassen",
|
||||
"Liste von Zahlen erfassen",
|
||||
"Erfasst mehrere numerische Werte über vordefinierte Eingabefelder.",
|
||||
"vaadin:list-ol",
|
||||
"#0369a1",
|
||||
13,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.fieldList("fields", "Zahlenfelder (Titel + Platzhalter)"),
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||
)
|
||||
));
|
||||
|
||||
register(new BlockType(
|
||||
"checkbox-liste-erfassen",
|
||||
"Liste von Checkboxen erfassen",
|
||||
"Lässt mehrere vordefinierte Optionen per Checkbox ankreuzen.",
|
||||
"vaadin:tasks",
|
||||
"#15803d",
|
||||
14,
|
||||
1, 1,
|
||||
List.of(
|
||||
PropertyDefinition.nameList("entries", "Name der Checkboxen"),
|
||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||
)
|
||||
));
|
||||
|
||||
if (!hideScriptBlock) {
|
||||
register(new BlockType(
|
||||
"script",
|
||||
@@ -217,7 +312,8 @@ public class BlockTypeRegistry {
|
||||
taskType,
|
||||
base.inputs(),
|
||||
base.outputs(),
|
||||
List.copyOf(properties)
|
||||
List.copyOf(properties),
|
||||
base.id()
|
||||
);
|
||||
register(custom);
|
||||
return custom;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package de.assecutor.tasklisteditor.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
/**
|
||||
* Ein einzelnes Eingabefeld eines Listen-Text-Tasks (Typ 12).
|
||||
* Definiert Titel und Platzhalter eines vom Bearbeiter auszufüllenden
|
||||
* Textfeldes.
|
||||
*
|
||||
* <p>{@code null}-Felder werden ausgelassen.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonPropertyOrder({"title", "placeholder"})
|
||||
public class FieldDto {
|
||||
|
||||
private String title;
|
||||
private String placeholder;
|
||||
|
||||
public FieldDto() {
|
||||
}
|
||||
|
||||
public FieldDto(String title, String placeholder) {
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getPlaceholder() {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
public void setPlaceholder(String placeholder) {
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.assecutor.tasklisteditor.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PropertyDefinition(String id, String label, PropertyType type, Object defaultValue) {
|
||||
public static PropertyDefinition text(String id, String label) {
|
||||
return new PropertyDefinition(id, label, PropertyType.TEXT, "");
|
||||
@@ -16,4 +18,12 @@ public record PropertyDefinition(String id, String label, PropertyType type, Obj
|
||||
public static PropertyDefinition bool(String id, String label, boolean defaultValue) {
|
||||
return new PropertyDefinition(id, label, PropertyType.BOOLEAN, defaultValue);
|
||||
}
|
||||
|
||||
public static PropertyDefinition fieldList(String id, String label) {
|
||||
return new PropertyDefinition(id, label, PropertyType.FIELD_LIST, List.of());
|
||||
}
|
||||
|
||||
public static PropertyDefinition nameList(String id, String label) {
|
||||
return new PropertyDefinition(id, label, PropertyType.NAME_LIST, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,9 @@ public enum PropertyType {
|
||||
TEXT,
|
||||
TEXTAREA,
|
||||
NUMBER,
|
||||
BOOLEAN
|
||||
BOOLEAN,
|
||||
/** Wiederholbare Liste aus Eingabefeldern mit Titel und Platzhalter. */
|
||||
FIELD_LIST,
|
||||
/** Wiederholbare Liste aus reinen Namens-Einträgen (ein Textfeld je Zeile). */
|
||||
NAME_LIST
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonPropertyOrder({"s_id", "type", "topic", "opt", "reenter", "min", "max",
|
||||
"max_len", "rt", "track", "txt", "task_arrival", "task_fin_work", "entries", "choices", "script"})
|
||||
"max_len", "rt", "track", "txt", "task_arrival", "task_fin_work", "entries", "choices", "fields", "script"})
|
||||
public class TaskDto {
|
||||
|
||||
@JsonProperty("s_id")
|
||||
@@ -50,6 +50,9 @@ public class TaskDto {
|
||||
/** Auswahloptionen eines Status-Tasks (Typ 6). */
|
||||
private List<ChoiceDto> choices;
|
||||
|
||||
/** Eingabefelder eines Listen-Text-Tasks (Typ 12). */
|
||||
private List<FieldDto> fields;
|
||||
|
||||
/** Logik-Graph eines Script-Blocks (Drawflow-Definition als JSON). */
|
||||
private JsonNode script;
|
||||
|
||||
@@ -173,6 +176,14 @@ public class TaskDto {
|
||||
this.choices = choices;
|
||||
}
|
||||
|
||||
public List<FieldDto> getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
public void setFields(List<FieldDto> fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
public JsonNode getScript() {
|
||||
return script;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,9 @@ public class CustomTypeService {
|
||||
doc.getOutputs(),
|
||||
doc.getProperties() == null ? List.of()
|
||||
: doc.getProperties().stream()
|
||||
.map(CustomPropertyState::toDefinition)
|
||||
.toList()
|
||||
.map(prop -> prop.toDefinition())
|
||||
.toList(),
|
||||
doc.getBaseTypeId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.assecutor.tasklisteditor.service;
|
||||
|
||||
import de.assecutor.tasklisteditor.config.ElementCollectionProperties;
|
||||
import de.assecutor.tasklisteditor.document.ElementSettingsDocument;
|
||||
import de.assecutor.tasklisteditor.model.BlockInstance;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* Speichert die Einstellungen der einzelnen platzierten Elemente eines Canvas
|
||||
* jeweils in einer eigenen, pro Element-Typ konfigurierten MongoDB-Collection.
|
||||
*
|
||||
* <p>Wird ergänzend zum {@link CanvasService} aufgerufen: Der Canvas-Stand
|
||||
* (Layout + Block-Werte) bleibt unverändert im Canvas-Dokument; zusätzlich wird
|
||||
* je Element ein {@link ElementSettingsDocument} in die Typ-Collection
|
||||
* geschrieben und über die Canvas-ID verknüpft.
|
||||
*/
|
||||
@Service
|
||||
public class ElementSettingsService {
|
||||
|
||||
private final MongoTemplate mongoTemplate;
|
||||
private final ElementCollectionProperties properties;
|
||||
|
||||
public ElementSettingsService(MongoTemplate mongoTemplate,
|
||||
ElementCollectionProperties properties) {
|
||||
this.mongoTemplate = mongoTemplate;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt für jedes Element des Canvas ein Einstellungs-Dokument in die
|
||||
* Collection seines Typs.
|
||||
*
|
||||
* @param canvasId ID des gespeicherten Canvas (Verknüpfung)
|
||||
* @param canvasName Anzeigename des Canvas
|
||||
* @param instances platzierte Block-Instanzen
|
||||
*/
|
||||
public void saveForCanvas(String canvasId, String canvasName,
|
||||
Collection<BlockInstance> instances) {
|
||||
Instant now = Instant.now();
|
||||
for (BlockInstance instance : instances) {
|
||||
// Virtuelle Blöcke (Start/Abschluss) haben keine fachlichen
|
||||
// Einstellungen und keine Collection – sie werden nicht persistiert.
|
||||
if (instance.type().isVirtual()) {
|
||||
continue;
|
||||
}
|
||||
String typeId = instance.type().id();
|
||||
String baseTypeId = instance.type().baseTypeId();
|
||||
// Benutzerdefinierte Typen landen in der Collection ihres statischen
|
||||
// Basistyps; feste Typen (baseTypeId == null) in ihrer eigenen.
|
||||
String collectionTypeId = (baseTypeId == null || baseTypeId.isBlank())
|
||||
? typeId
|
||||
: baseTypeId;
|
||||
ElementSettingsDocument document = new ElementSettingsDocument(
|
||||
canvasId,
|
||||
canvasName,
|
||||
instance.id(),
|
||||
typeId,
|
||||
baseTypeId,
|
||||
instance.topic(),
|
||||
new LinkedHashMap<>(instance.values()),
|
||||
now);
|
||||
mongoTemplate.save(document, properties.collectionFor(collectionTypeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package de.assecutor.tasklisteditor.service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.assecutor.tasklisteditor.model.BlockInstance;
|
||||
import de.assecutor.tasklisteditor.model.BlockTypeRegistry;
|
||||
import de.assecutor.tasklisteditor.model.ChoiceDto;
|
||||
import de.assecutor.tasklisteditor.model.FieldDto;
|
||||
import de.assecutor.tasklisteditor.model.TaskDto;
|
||||
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -47,6 +49,10 @@ public class TaskListExporter {
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Editor-Graph konnte nicht gelesen werden: " + ex.getMessage(), ex);
|
||||
}
|
||||
// Ein vollständiger Workflow muss mit einem Start- und einem Abschluss-
|
||||
// Element gerahmt und beide müssen verdrahtet sein.
|
||||
validateWorkflow(data, instances);
|
||||
|
||||
if (!data.isObject() || data.isEmpty()) {
|
||||
return tasks;
|
||||
}
|
||||
@@ -59,6 +65,11 @@ public class TaskListExporter {
|
||||
if (instance == null) {
|
||||
continue;
|
||||
}
|
||||
// Rein virtuelle Blöcke (Start/Abschluss) sind nur Workflow-Markierungen
|
||||
// und gehören nicht in die exportierte Task-Liste.
|
||||
if (instance.type().isVirtual()) {
|
||||
continue;
|
||||
}
|
||||
TaskDto task = toTask(instance);
|
||||
task.setSId(sId++);
|
||||
tasks.add(task);
|
||||
@@ -66,6 +77,64 @@ public class TaskListExporter {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass der Ablauf mit genau den virtuellen Markierungen
|
||||
* gerahmt ist: Es muss mindestens ein Start- und ein Abschluss-Element
|
||||
* geben und beide müssen mit dem Ablauf verbunden (verdrahtet) sein.
|
||||
*
|
||||
* @throws IllegalStateException mit einer für den Benutzer verständlichen
|
||||
* 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();
|
||||
if (taskType == BlockTypeRegistry.START_TASK_TYPE) {
|
||||
startNodes.add(entry.getKey());
|
||||
} else if (taskType == BlockTypeRegistry.END_TASK_TYPE) {
|
||||
endNodes.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (startNodes.isEmpty()) {
|
||||
throw new IllegalStateException("Es muss ein Start-Element vorhanden sein.");
|
||||
}
|
||||
if (endNodes.isEmpty()) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Integer> determineOrder(JsonNode data) {
|
||||
// Knoten einsammeln + eingehende Verbindungen zählen.
|
||||
List<Integer> nodeIds = new ArrayList<>();
|
||||
@@ -144,13 +213,20 @@ public class TaskListExporter {
|
||||
|
||||
if (v.containsKey("entries")) {
|
||||
boolean queryRemark = v.containsKey("query_remark") && asBool(v.get("query_remark"));
|
||||
task.setEntries(parseEntries(asText(v.get("entries")), queryRemark));
|
||||
Object raw = v.get("entries");
|
||||
task.setEntries(raw instanceof List<?> list
|
||||
? parseEntries(list, queryRemark)
|
||||
: parseEntries(asText(raw), queryRemark));
|
||||
}
|
||||
|
||||
if (v.containsKey("choices")) {
|
||||
task.setChoices(parseChoices(asText(v.get("choices"))));
|
||||
}
|
||||
|
||||
if (v.containsKey("fields")) {
|
||||
task.setFields(parseFields(v.get("fields")));
|
||||
}
|
||||
|
||||
if (v.containsKey("script")) {
|
||||
String script = asText(v.get("script"));
|
||||
if (script != null) {
|
||||
@@ -179,6 +255,21 @@ public class TaskListExporter {
|
||||
return entries.isEmpty() ? null : entries;
|
||||
}
|
||||
|
||||
/** Variante für den Zeilen-Editor: Liste aus {@code {title}}-Einträgen. */
|
||||
private List<TaskEntryDto> parseEntries(List<?> raw, boolean queryRemark) {
|
||||
List<TaskEntryDto> entries = new ArrayList<>();
|
||||
int code = 1;
|
||||
for (Object o : raw) {
|
||||
if (o instanceof Map<?, ?> m) {
|
||||
String text = m.get("title") == null ? "" : m.get("title").toString().trim();
|
||||
if (!text.isEmpty()) {
|
||||
entries.add(new TaskEntryDto(code++, text, queryRemark));
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries.isEmpty() ? null : entries;
|
||||
}
|
||||
|
||||
private List<ChoiceDto> parseChoices(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
@@ -194,6 +285,23 @@ public class TaskListExporter {
|
||||
return choices.isEmpty() ? null : choices;
|
||||
}
|
||||
|
||||
private List<FieldDto> parseFields(Object raw) {
|
||||
if (!(raw instanceof List<?> list) || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<FieldDto> fields = new ArrayList<>();
|
||||
for (Object o : list) {
|
||||
if (o instanceof Map<?, ?> m) {
|
||||
String title = m.get("title") == null ? "" : m.get("title").toString().trim();
|
||||
String placeholder = m.get("placeholder") == null ? "" : m.get("placeholder").toString().trim();
|
||||
if (!title.isEmpty() || !placeholder.isEmpty()) {
|
||||
fields.add(new FieldDto(title, placeholder));
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields.isEmpty() ? null : fields;
|
||||
}
|
||||
|
||||
private Boolean asBool(Object o) {
|
||||
return o instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(o));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.assecutor.tasklisteditor.service;
|
||||
|
||||
import de.assecutor.tasklisteditor.model.BlockType;
|
||||
import de.assecutor.tasklisteditor.model.BlockTypeRegistry;
|
||||
import de.assecutor.tasklisteditor.model.ChoiceDto;
|
||||
import de.assecutor.tasklisteditor.model.FieldDto;
|
||||
import de.assecutor.tasklisteditor.model.PropertyDefinition;
|
||||
import de.assecutor.tasklisteditor.model.TaskDto;
|
||||
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
||||
@@ -63,7 +63,21 @@ public class TaskListImporter {
|
||||
private Map<String, Object> valuesFor(BlockType type, TaskDto task) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
for (PropertyDefinition def : type.properties()) {
|
||||
Object value = switch (def.id()) {
|
||||
// Listen-Eigenschaften (Typ 12–14) werden anhand ihres
|
||||
// Property-Typs in die vom Zeilen-Editor erwartete Struktur
|
||||
// (Liste aus {title}/{title,placeholder}-Maps) zurückgewandelt.
|
||||
Object value = switch (def.type()) {
|
||||
case FIELD_LIST -> fieldRows(task);
|
||||
case NAME_LIST -> entryRows(task);
|
||||
default -> scalarValue(def, task);
|
||||
};
|
||||
values.put(def.id(), value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Object scalarValue(PropertyDefinition def, TaskDto task) {
|
||||
return switch (def.id()) {
|
||||
case "opt" -> orDefault(task.getOpt(), def.defaultValue());
|
||||
case "reenter" -> orDefault(task.getReenter(), def.defaultValue());
|
||||
case "rt" -> orDefault(task.getRt(), def.defaultValue());
|
||||
@@ -77,9 +91,35 @@ public class TaskListImporter {
|
||||
case "choices" -> choicesText(task);
|
||||
default -> def.defaultValue();
|
||||
};
|
||||
values.put(def.id(), value);
|
||||
}
|
||||
return values;
|
||||
|
||||
/** {@code fields}-Array (Typ 12/13) zurück in Zeilen mit Titel + Platzhalter. */
|
||||
private List<Map<String, Object>> fieldRows(TaskDto task) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (task.getFields() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (FieldDto field : task.getFields()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("title", field.getTitle() == null ? "" : field.getTitle());
|
||||
row.put("placeholder", field.getPlaceholder() == null ? "" : field.getPlaceholder());
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** {@code entries}-Array (Typ 14) zurück in Zeilen mit Namen ({@code title}). */
|
||||
private List<Map<String, Object>> entryRows(TaskDto task) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (task.getEntries() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (TaskEntryDto entry : task.getEntries()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("title", entry.getText() == null ? "" : entry.getText());
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private Object orDefault(Object value, Object fallback) {
|
||||
@@ -88,7 +128,7 @@ public class TaskListImporter {
|
||||
|
||||
private boolean anyQueryRemark(TaskDto task) {
|
||||
return task.getEntries() != null
|
||||
&& task.getEntries().stream().anyMatch(TaskEntryDto::isQueryRemark);
|
||||
&& task.getEntries().stream().anyMatch(entry -> entry.isQueryRemark());
|
||||
}
|
||||
|
||||
private String entriesText(TaskDto task) {
|
||||
@@ -96,14 +136,14 @@ public class TaskListImporter {
|
||||
return "";
|
||||
}
|
||||
return task.getEntries().stream()
|
||||
.map(TaskEntryDto::getText)
|
||||
.map(entry -> entry.getText())
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private String choicesText(TaskDto task) {
|
||||
if (task.getChoices() != null && !task.getChoices().isEmpty()) {
|
||||
return task.getChoices().stream()
|
||||
.map(ChoiceDto::getTxt)
|
||||
.map(choice -> choice.getTxt())
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
// Altdaten des früheren Typ 11 (Abhol-/Lieferstatus) liefern die Optionen
|
||||
|
||||
@@ -32,8 +32,11 @@ public class TaskListService {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/** Task-Typ des Script-Blocks – wird von der stadtbote-App nicht verarbeitet. */
|
||||
private static final int SCRIPT_TASK_TYPE = 100;
|
||||
/**
|
||||
* Task-Typ des Script-Blocks (siehe {@code BlockTypeRegistry}) – wird von der
|
||||
* stadtbote-App nicht verarbeitet und daher aus dem App-JSON ausgelassen.
|
||||
*/
|
||||
private static final int SCRIPT_TASK_TYPE = 0;
|
||||
|
||||
/** Ergebnis eines Exports: typisierte Tasks plus formatiertes JSON. */
|
||||
public record ExportResult(List<TaskDto> tasks, String json) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import de.assecutor.tasklisteditor.model.TaskListPayload;
|
||||
import de.assecutor.tasklisteditor.document.CanvasDocument;
|
||||
import de.assecutor.tasklisteditor.service.CanvasService;
|
||||
import de.assecutor.tasklisteditor.service.CustomTypeService;
|
||||
import de.assecutor.tasklisteditor.service.ElementSettingsService;
|
||||
import de.assecutor.tasklisteditor.service.PendingImportStore;
|
||||
import de.assecutor.tasklisteditor.service.TaskListImporter;
|
||||
import de.assecutor.tasklisteditor.service.TaskListService;
|
||||
@@ -72,6 +73,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
private final PendingImportStore importStore;
|
||||
private final CanvasService canvasService;
|
||||
private final CustomTypeService customTypeService;
|
||||
private final ElementSettingsService elementSettingsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final NodeEditor nodeEditor;
|
||||
private final Map<Integer, BlockInstance> instances = new HashMap<>();
|
||||
@@ -85,6 +87,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
PendingImportStore importStore,
|
||||
CanvasService canvasService,
|
||||
CustomTypeService customTypeService,
|
||||
ElementSettingsService elementSettingsService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.registry = registry;
|
||||
this.taskListService = taskListService;
|
||||
@@ -92,6 +95,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
this.importStore = importStore;
|
||||
this.canvasService = canvasService;
|
||||
this.customTypeService = customTypeService;
|
||||
this.elementSettingsService = elementSettingsService;
|
||||
this.objectMapper = objectMapper;
|
||||
setSizeFull();
|
||||
setPadding(false);
|
||||
@@ -432,6 +436,10 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
* Canvas-Graphen ermittelt, um sie als Variablen anzubieten.
|
||||
*/
|
||||
private void openSettingsDialog(BlockInstance instance) {
|
||||
// Rein virtuelle Elemente (Start/Abschluss) haben keine Einstellungen.
|
||||
if (instance.type().isVirtual()) {
|
||||
return;
|
||||
}
|
||||
if ("script".equals(instance.type().id())) {
|
||||
nodeEditor.exportGraph(graphJson -> openScriptDialog(instance, graphJson));
|
||||
return;
|
||||
@@ -712,10 +720,111 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
box.addValueChangeListener(e -> instance.set(def.id(), e.getValue()));
|
||||
return box;
|
||||
}
|
||||
case FIELD_LIST -> {
|
||||
return buildRowList(instance, def, current, true);
|
||||
}
|
||||
case NAME_LIST -> {
|
||||
return buildRowList(instance, def, current, false);
|
||||
}
|
||||
}
|
||||
return new Span();
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor für eine wiederholbare Liste von Eingabefeldern. Jede Zeile besteht
|
||||
* aus einem Titel und – falls {@code withPlaceholder} – zusätzlich einem
|
||||
* Platzhalter. Der Wert wird als {@code List<Map<String,Object>>} unter der
|
||||
* Property-ID abgelegt (rundläuft über Mongo und Export).
|
||||
*/
|
||||
private Component buildRowList(BlockInstance instance, PropertyDefinition def, Object current,
|
||||
boolean withPlaceholder) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (current instanceof List<?> list) {
|
||||
for (Object o : list) {
|
||||
if (o instanceof Map<?, ?> m) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("title", m.get("title") == null ? "" : m.get("title").toString());
|
||||
if (withPlaceholder) {
|
||||
row.put("placeholder", m.get("placeholder") == null ? "" : m.get("placeholder").toString());
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout wrapper = new VerticalLayout();
|
||||
wrapper.setPadding(false);
|
||||
wrapper.setSpacing(false);
|
||||
wrapper.setWidthFull();
|
||||
|
||||
Span label = new Span(def.label());
|
||||
label.addClassName("properties-fieldlist-label");
|
||||
wrapper.add(label);
|
||||
|
||||
VerticalLayout rowsBox = new VerticalLayout();
|
||||
rowsBox.setPadding(false);
|
||||
rowsBox.setSpacing(true);
|
||||
rowsBox.setWidthFull();
|
||||
wrapper.add(rowsBox);
|
||||
|
||||
Runnable commit = () -> instance.set(def.id(), new ArrayList<>(rows));
|
||||
Runnable[] render = new Runnable[1];
|
||||
render[0] = () -> {
|
||||
rowsBox.removeAll();
|
||||
for (Map<String, Object> row : rows) {
|
||||
TextField title = new TextField();
|
||||
title.setPlaceholder(withPlaceholder ? "Titel" : "Name");
|
||||
title.setValue(row.get("title").toString());
|
||||
title.addValueChangeListener(e -> {
|
||||
row.put("title", e.getValue());
|
||||
commit.run();
|
||||
});
|
||||
Button remove = new Button(new Icon(VaadinIcon.TRASH), ev -> {
|
||||
rows.remove(row);
|
||||
commit.run();
|
||||
render[0].run();
|
||||
});
|
||||
remove.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE, ButtonVariant.LUMO_ERROR);
|
||||
|
||||
HorizontalLayout line = new HorizontalLayout(title);
|
||||
line.setWidthFull();
|
||||
line.setFlexGrow(1, title);
|
||||
if (withPlaceholder) {
|
||||
TextField placeholder = new TextField();
|
||||
placeholder.setPlaceholder("Platzhalter");
|
||||
placeholder.setValue(row.get("placeholder").toString());
|
||||
placeholder.addValueChangeListener(e -> {
|
||||
row.put("placeholder", e.getValue());
|
||||
commit.run();
|
||||
});
|
||||
line.add(placeholder);
|
||||
line.setFlexGrow(1, placeholder);
|
||||
}
|
||||
line.add(remove);
|
||||
line.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||
rowsBox.add(line);
|
||||
}
|
||||
};
|
||||
render[0].run();
|
||||
|
||||
Button add = new Button(withPlaceholder ? "Feld hinzufügen" : "Eintrag hinzufügen",
|
||||
new Icon(VaadinIcon.PLUS), ev -> {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("title", "");
|
||||
if (withPlaceholder) {
|
||||
row.put("placeholder", "");
|
||||
}
|
||||
rows.add(row);
|
||||
commit.run();
|
||||
render[0].run();
|
||||
});
|
||||
add.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
wrapper.add(add);
|
||||
|
||||
commit.run();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEnter(BeforeEnterEvent event) {
|
||||
List<String> tokens = event.getLocation().getQueryParameters()
|
||||
@@ -793,6 +902,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
Button save = new Button("Speichern", new Icon(VaadinIcon.HARDDRIVE), ev -> {
|
||||
try {
|
||||
String id = canvasService.save(name.getValue(), layout, collectBlocks());
|
||||
elementSettingsService.saveForCanvas(id, name.getValue(), instances.values());
|
||||
currentListName = name.getValue();
|
||||
Notification.show("Canvas gespeichert (ID: " + id + ")");
|
||||
dialog.close();
|
||||
@@ -840,7 +950,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
Grid<CanvasDocument> grid = new Grid<>();
|
||||
grid.addColumn(CanvasDocument::getName).setHeader("Name").setAutoWidth(true);
|
||||
grid.addColumn(d -> d.getName()).setHeader("Name").setAutoWidth(true);
|
||||
grid.addColumn(d -> d.getBlocks() == null ? 0 : d.getBlocks().size()).setHeader("Blöcke");
|
||||
grid.addColumn(d -> d.getCreatedAt() == null ? "" : fmt.format(d.getCreatedAt()))
|
||||
.setHeader("Gespeichert");
|
||||
|
||||
@@ -25,3 +25,22 @@ management.health.mongo.enabled=false
|
||||
# Script-Block (grafisches Logik-Element) im Editor ausblenden.
|
||||
# Wird per .env (HIDE_SCRIPT_BLOCK) gesteuert.
|
||||
tasklist.hide-script-block=${HIDE_SCRIPT_BLOCK:false}
|
||||
|
||||
# --- Collections je Element-Typ ---------------------------------------------
|
||||
# Einstellungen der einzelnen Elemente werden beim Canvas-Speichern zusätzlich
|
||||
# je Element-Typ in eine eigene Collection geschrieben. Namen aus der .env
|
||||
# (MONGODB_ELEMENT_*), mit Default. Benutzerdefinierte Typen werden auf ihren
|
||||
# statischen Basistyp aufgelöst; für jeden Typ muss eine Zuordnung existieren.
|
||||
tasklist.element-collections[ankunft-melden]=${MONGODB_ELEMENT_ARRIVAL_COLLECTION:element_arrival}
|
||||
tasklist.element-collections[unterschrift-erfassen]=${MONGODB_ELEMENT_SIGNATURE_COLLECTION:element_signature}
|
||||
tasklist.element-collections[fotos-aufnehmen]=${MONGODB_ELEMENT_PHOTOS_COLLECTION:element_photos}
|
||||
tasklist.element-collections[quittungsgeber-erfassen]=${MONGODB_ELEMENT_RECEIPT_GIVER_COLLECTION:element_receipt_giver}
|
||||
tasklist.element-collections[bemerkung-erfassen]=${MONGODB_ELEMENT_REMARK_COLLECTION:element_remark}
|
||||
tasklist.element-collections[kommissionsnummer-erfassen]=${MONGODB_ELEMENT_COMMISSION_NUMBER_COLLECTION:element_commission_number}
|
||||
tasklist.element-collections[checkbox]=${MONGODB_ELEMENT_CHECKBOX_COLLECTION:element_checkbox}
|
||||
tasklist.element-collections[barcode-scannen]=${MONGODB_ELEMENT_BARCODE_COLLECTION:element_barcode}
|
||||
tasklist.element-collections[statusauswahl]=${MONGODB_ELEMENT_STATUS_SELECTION_COLLECTION:element_status_selection}
|
||||
tasklist.element-collections[text-liste-erfassen]=${MONGODB_ELEMENT_TEXT_LIST_COLLECTION:element_text_list}
|
||||
tasklist.element-collections[zahl-liste-erfassen]=${MONGODB_ELEMENT_NUMBER_LIST_COLLECTION:element_number_list}
|
||||
tasklist.element-collections[checkbox-liste-erfassen]=${MONGODB_ELEMENT_CHECKBOX_LIST_COLLECTION:element_checkbox_list}
|
||||
tasklist.element-collections[script]=${MONGODB_ELEMENT_SCRIPT_COLLECTION:element_script}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"noUnusedParameters": false,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": "src/main/frontend",
|
||||
"paths": {
|
||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||
|
||||
Reference in New Issue
Block a user