diff --git a/.env.example b/.env.example index 0206298..98430ee 100644 --- a/.env.example +++ b/.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) diff --git a/src/main/frontend/node-editor/node-editor.js b/src/main/frontend/node-editor/node-editor.js index 65b4a80..2957196 100644 --- a/src/main/frontend/node-editor/node-editor.js +++ b/src/main/frontend/node-editor/node-editor.js @@ -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; } diff --git a/src/main/frontend/themes/tasklist-editor/styles.css b/src/main/frontend/themes/tasklist-editor/styles.css index 7ce7e8c..6933995 100644 --- a/src/main/frontend/themes/tasklist-editor/styles.css +++ b/src/main/frontend/themes/tasklist-editor/styles.css @@ -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 { diff --git a/src/main/java/de/assecutor/tasklisteditor/config/ElementCollectionProperties.java b/src/main/java/de/assecutor/tasklisteditor/config/ElementCollectionProperties.java new file mode 100644 index 0000000..d4699de --- /dev/null +++ b/src/main/java/de/assecutor/tasklisteditor/config/ElementCollectionProperties.java @@ -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. + * + *

Die Collection-Namen werden aus {@code application.properties} gebunden, + * deren Werte wiederum aus den {@code MONGODB_ELEMENT_*}-Variablen der + * {@code .env} stammen: + *

+ *   tasklist.element-collections[text-liste-erfassen]=${MONGODB_ELEMENT_TEXT_LISTE_COLLECTION:element_text_liste_erfassen}
+ * 
+ * + *

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 elementCollections = new HashMap<>(); + + public Map 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; + } +} diff --git a/src/main/java/de/assecutor/tasklisteditor/document/ElementSettingsDocument.java b/src/main/java/de/assecutor/tasklisteditor/document/ElementSettingsDocument.java new file mode 100644 index 0000000..a6f0618 --- /dev/null +++ b/src/main/java/de/assecutor/tasklisteditor/document/ElementSettingsDocument.java @@ -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}). + * + *

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 values; + + private Instant createdAt; + + public ElementSettingsDocument() { + } + + public ElementSettingsDocument(String canvasId, String canvasName, int nodeId, + String typeId, String baseTypeId, String topic, + Map 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 getValues() { + return values; + } + + public void setValues(Map values) { + this.values = values; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} diff --git a/src/main/java/de/assecutor/tasklisteditor/model/BlockType.java b/src/main/java/de/assecutor/tasklisteditor/model/BlockType.java index 9417868..2cb93ff 100644 --- a/src/main/java/de/assecutor/tasklisteditor/model/BlockType.java +++ b/src/main/java/de/assecutor/tasklisteditor/model/BlockType.java @@ -11,6 +11,22 @@ public record BlockType( int taskType, int inputs, int outputs, - List properties + List 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 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; + } } diff --git a/src/main/java/de/assecutor/tasklisteditor/model/BlockTypeRegistry.java b/src/main/java/de/assecutor/tasklisteditor/model/BlockTypeRegistry.java index 862cf6a..686a944 100644 --- a/src/main/java/de/assecutor/tasklisteditor/model/BlockTypeRegistry.java +++ b/src/main/java/de/assecutor/tasklisteditor/model/BlockTypeRegistry.java @@ -22,7 +22,39 @@ public class BlockTypeRegistry { private final Map 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; diff --git a/src/main/java/de/assecutor/tasklisteditor/model/FieldDto.java b/src/main/java/de/assecutor/tasklisteditor/model/FieldDto.java new file mode 100644 index 0000000..e281107 --- /dev/null +++ b/src/main/java/de/assecutor/tasklisteditor/model/FieldDto.java @@ -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. + * + *

{@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; + } +} diff --git a/src/main/java/de/assecutor/tasklisteditor/model/PropertyDefinition.java b/src/main/java/de/assecutor/tasklisteditor/model/PropertyDefinition.java index 9e34313..ec7d72a 100644 --- a/src/main/java/de/assecutor/tasklisteditor/model/PropertyDefinition.java +++ b/src/main/java/de/assecutor/tasklisteditor/model/PropertyDefinition.java @@ -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()); + } } diff --git a/src/main/java/de/assecutor/tasklisteditor/model/PropertyType.java b/src/main/java/de/assecutor/tasklisteditor/model/PropertyType.java index 29f060f..7d99f81 100644 --- a/src/main/java/de/assecutor/tasklisteditor/model/PropertyType.java +++ b/src/main/java/de/assecutor/tasklisteditor/model/PropertyType.java @@ -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 } diff --git a/src/main/java/de/assecutor/tasklisteditor/model/TaskDto.java b/src/main/java/de/assecutor/tasklisteditor/model/TaskDto.java index 356a828..176dac6 100644 --- a/src/main/java/de/assecutor/tasklisteditor/model/TaskDto.java +++ b/src/main/java/de/assecutor/tasklisteditor/model/TaskDto.java @@ -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 choices; + /** Eingabefelder eines Listen-Text-Tasks (Typ 12). */ + private List 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 getFields() { + return fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + public JsonNode getScript() { return script; } diff --git a/src/main/java/de/assecutor/tasklisteditor/service/CustomTypeService.java b/src/main/java/de/assecutor/tasklisteditor/service/CustomTypeService.java index 9944968..6529bba 100644 --- a/src/main/java/de/assecutor/tasklisteditor/service/CustomTypeService.java +++ b/src/main/java/de/assecutor/tasklisteditor/service/CustomTypeService.java @@ -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() ); } } diff --git a/src/main/java/de/assecutor/tasklisteditor/service/ElementSettingsService.java b/src/main/java/de/assecutor/tasklisteditor/service/ElementSettingsService.java new file mode 100644 index 0000000..eb0ae0f --- /dev/null +++ b/src/main/java/de/assecutor/tasklisteditor/service/ElementSettingsService.java @@ -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. + * + *

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 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)); + } + } +} diff --git a/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java b/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java index 2169eb3..059527e 100644 --- a/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java +++ b/src/main/java/de/assecutor/tasklisteditor/service/TaskListExporter.java @@ -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 instances) { + List startNodes = new ArrayList<>(); + List endNodes = new ArrayList<>(); + for (Map.Entry 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 determineOrder(JsonNode data) { // Knoten einsammeln + eingehende Verbindungen zählen. List 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 parseEntries(List raw, boolean queryRemark) { + List 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 parseChoices(String raw) { if (raw == null || raw.isBlank()) { return null; @@ -194,6 +285,23 @@ public class TaskListExporter { return choices.isEmpty() ? null : choices; } + private List parseFields(Object raw) { + if (!(raw instanceof List list) || list.isEmpty()) { + return null; + } + List 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)); } diff --git a/src/main/java/de/assecutor/tasklisteditor/service/TaskListImporter.java b/src/main/java/de/assecutor/tasklisteditor/service/TaskListImporter.java index 078ef24..7ad6e75 100644 --- a/src/main/java/de/assecutor/tasklisteditor/service/TaskListImporter.java +++ b/src/main/java/de/assecutor/tasklisteditor/service/TaskListImporter.java @@ -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,32 +63,72 @@ public class TaskListImporter { private Map valuesFor(BlockType type, TaskDto task) { Map values = new LinkedHashMap<>(); for (PropertyDefinition def : type.properties()) { - Object value = switch (def.id()) { - case "opt" -> orDefault(task.getOpt(), def.defaultValue()); - case "reenter" -> orDefault(task.getReenter(), def.defaultValue()); - case "rt" -> orDefault(task.getRt(), def.defaultValue()); - case "track" -> orDefault(task.getTrack(), def.defaultValue()); - case "min" -> orDefault(task.getMin(), def.defaultValue()); - case "max" -> orDefault(task.getMax(), def.defaultValue()); - case "max_len" -> orDefault(task.getMaxLen(), def.defaultValue()); - case "txt" -> orDefault(task.getTxt(), def.defaultValue()); - case "query_remark" -> anyQueryRemark(task); - case "entries" -> entriesText(task); - case "choices" -> choicesText(task); - default -> def.defaultValue(); + // 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()); + case "track" -> orDefault(task.getTrack(), def.defaultValue()); + case "min" -> orDefault(task.getMin(), def.defaultValue()); + case "max" -> orDefault(task.getMax(), def.defaultValue()); + case "max_len" -> orDefault(task.getMaxLen(), def.defaultValue()); + case "txt" -> orDefault(task.getTxt(), def.defaultValue()); + case "query_remark" -> anyQueryRemark(task); + case "entries" -> entriesText(task); + case "choices" -> choicesText(task); + default -> def.defaultValue(); + }; + } + + /** {@code fields}-Array (Typ 12/13) zurück in Zeilen mit Titel + Platzhalter. */ + private List> fieldRows(TaskDto task) { + List> rows = new ArrayList<>(); + if (task.getFields() == null) { + return rows; + } + for (FieldDto field : task.getFields()) { + Map 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> entryRows(TaskDto task) { + List> rows = new ArrayList<>(); + if (task.getEntries() == null) { + return rows; + } + for (TaskEntryDto entry : task.getEntries()) { + Map row = new LinkedHashMap<>(); + row.put("title", entry.getText() == null ? "" : entry.getText()); + rows.add(row); + } + return rows; + } + private Object orDefault(Object value, Object fallback) { return value != null ? value : fallback; } 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 diff --git a/src/main/java/de/assecutor/tasklisteditor/service/TaskListService.java b/src/main/java/de/assecutor/tasklisteditor/service/TaskListService.java index d025361..017566f 100644 --- a/src/main/java/de/assecutor/tasklisteditor/service/TaskListService.java +++ b/src/main/java/de/assecutor/tasklisteditor/service/TaskListService.java @@ -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 tasks, String json) { diff --git a/src/main/java/de/assecutor/tasklisteditor/view/MainView.java b/src/main/java/de/assecutor/tasklisteditor/view/MainView.java index ff1685d..a482679 100644 --- a/src/main/java/de/assecutor/tasklisteditor/view/MainView.java +++ b/src/main/java/de/assecutor/tasklisteditor/view/MainView.java @@ -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 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>} unter der + * Property-ID abgelegt (rundläuft über Mongo und Export). + */ + private Component buildRowList(BlockInstance instance, PropertyDefinition def, Object current, + boolean withPlaceholder) { + List> rows = new ArrayList<>(); + if (current instanceof List list) { + for (Object o : list) { + if (o instanceof Map m) { + Map 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 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 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 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 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"); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bc1d543..d1bc2f6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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} diff --git a/tsconfig.json b/tsconfig.json index f855f65..e508169 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"],