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:
2026-06-29 12:57:52 +02:00
parent 7d5fd3b590
commit cf9c73635b
19 changed files with 899 additions and 45 deletions

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.&nbsp;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;
}
}

View File

@@ -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 (111).
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;

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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()
);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}

View File

@@ -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<String, Object> valuesFor(BlockType type, TaskDto task) {
Map<String, Object> 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 1214) 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<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) {
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

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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}

View File

@@ -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"],