feat: Listen-Elemente, virtuelle Start/Abschluss-Blöcke und Workflow-Validierung
- Neue Aufgabentypen: Text-/Zahlen-Liste (fields) und Checkbox-Liste (entries); Export/Import um fields und choices erweitert - Typ-spezifische Element-Einstellungen in MongoDB (ElementSettingsService/-Document, ElementCollectionProperties) - Virtuelle Start-/Abschluss-Elemente (nur zur Darstellung, nicht exportiert); keine Einstellungen; Export erzwingt vorhandenes und verdrahtetes Start- und Abschluss-Element - Node-Editor: nur eine Verbindung je Ausgang (Start des Drags unterbunden) - Script-Block korrekt aus dem App-JSON gefiltert (Typ 0) - Fixes: tsconfig ignoreDeprecations 6.0; JDT-Null-Analyse (Method-Referenzen zu Lambdas) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
.env.example
18
.env.example
@@ -19,6 +19,24 @@ MONGODB_CANVAS_COLLECTION=canvas_layouts
|
|||||||
# Collection, in der die benutzerdefinierten Typen gespeichert werden:
|
# Collection, in der die benutzerdefinierten Typen gespeichert werden:
|
||||||
MONGODB_CUSTOM_TYPE_COLLECTION=custom_types
|
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:
|
# Script-Block (grafisches Logik-Element) ausblenden:
|
||||||
# true = Script-Element wird im Editor nicht angezeigt
|
# true = Script-Element wird im Editor nicht angezeigt
|
||||||
# false = Script-Element ist verfügbar (Standard)
|
# false = Script-Element ist verfügbar (Standard)
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import 'drawflow/dist/drawflow.min.css';
|
|||||||
// senkrecht in den Eingang des Ziels eintritt, bevor sie abbiegt.
|
// senkrecht in den Eingang des Ziels eintritt, bevor sie abbiegt.
|
||||||
const EXIT_STUB = 5;
|
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) {
|
function verticalCurvature(sx, sy, ex, ey) {
|
||||||
const dx = ex - sx;
|
const dx = ex - sx;
|
||||||
const minOffset = 40;
|
const minOffset = 40;
|
||||||
@@ -183,6 +188,8 @@ if (!window.__taskListEditorDragInit) {
|
|||||||
if (target && e.dataTransfer) {
|
if (target && e.dataTransfer) {
|
||||||
e.dataTransfer.setData('application/x-blocktype', target.dataset.blocktype);
|
e.dataTransfer.setData('application/x-blocktype', target.dataset.blocktype);
|
||||||
e.dataTransfer.effectAllowed = 'copy';
|
e.dataTransfer.effectAllowed = 'copy';
|
||||||
|
const r = target.getBoundingClientRect();
|
||||||
|
dragGrabOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
@@ -251,8 +258,10 @@ function attach(host) {
|
|||||||
|
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const zoom = editor.zoom;
|
const zoom = editor.zoom;
|
||||||
const px = (e.clientX - rect.left - editor.canvas_x) / zoom;
|
// Greif-Offset abziehen, damit der Knoten an seiner gezogenen Position
|
||||||
const py = (e.clientY - rect.top - editor.canvas_y) / zoom;
|
// 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);
|
addNodeInternal(host, def, px, py);
|
||||||
});
|
});
|
||||||
@@ -282,6 +291,40 @@ function attach(host) {
|
|||||||
host.$server && host.$server.onNodeRemoved(parseInt(id, 10));
|
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
|
// Klick in die Nähe einer Verbindung (auf leerem Canvas-Hintergrund) wählt
|
||||||
// diese aus – robuster als das exakte Treffen der dünnen Linie. Läuft in der
|
// diese aus – robuster als das exakte Treffen der dünnen Linie. Läuft in der
|
||||||
// Capture-Phase vor Drawflow, um das Verschieben/Deselektieren zu verhindern.
|
// Capture-Phase vor Drawflow, um das Verschieben/Deselektieren zu verhindern.
|
||||||
@@ -296,6 +339,64 @@ function attach(host) {
|
|||||||
selectConnection(state, path);
|
selectConnection(state, path);
|
||||||
}, true);
|
}, 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,
|
// Eine angeklickte Verbindung erhält den Fokus auf dem Canvas-Container,
|
||||||
// damit die Entf-/Backspace-Taste sie löschen kann.
|
// damit die Entf-/Backspace-Taste sie löschen kann.
|
||||||
editor.on('connectionSelected', () => {
|
editor.on('connectionSelected', () => {
|
||||||
@@ -317,7 +418,12 @@ function attach(host) {
|
|||||||
editor.removeConnection();
|
editor.removeConnection();
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', deleteSelectedConnection);
|
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;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ body, .tasklist-app {
|
|||||||
pointer-events: none;
|
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 {
|
.task-node-header {
|
||||||
height: 6px;
|
height: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -418,6 +424,13 @@ body, .tasklist-app {
|
|||||||
margin: 0 0 var(--lumo-space-s);
|
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-/Logik-Editor --- */
|
||||||
.script-editor,
|
.script-editor,
|
||||||
.script-editor-container {
|
.script-editor-container {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package de.assecutor.tasklisteditor.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguration der MongoDB-Collections, in denen die Einstellungen der
|
||||||
|
* einzelnen Element-Typen abgelegt werden.
|
||||||
|
*
|
||||||
|
* <p>Die Collection-Namen werden aus {@code application.properties} gebunden,
|
||||||
|
* deren Werte wiederum aus den {@code MONGODB_ELEMENT_*}-Variablen der
|
||||||
|
* {@code .env} stammen:
|
||||||
|
* <pre>
|
||||||
|
* tasklist.element-collections[text-liste-erfassen]=${MONGODB_ELEMENT_TEXT_LISTE_COLLECTION:element_text_liste_erfassen}
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>Für jeden (festen wie benutzerdefinierten) Typ muss eine Zuordnung
|
||||||
|
* existieren; benutzerdefinierte Typen werden zuvor auf ihren statischen
|
||||||
|
* Basistyp aufgelöst. Fehlt ein Eintrag, ist das ein Konfigurationsfehler.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "tasklist")
|
||||||
|
public class ElementCollectionProperties {
|
||||||
|
|
||||||
|
/** Typ-ID → Collection-Name. */
|
||||||
|
private final Map<String, String> elementCollections = new HashMap<>();
|
||||||
|
|
||||||
|
public Map<String, String> getElementCollections() {
|
||||||
|
return elementCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den Collection-Namen für die angegebene Typ-ID.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException wenn für die Typ-ID keine Zuordnung
|
||||||
|
* konfiguriert ist
|
||||||
|
*/
|
||||||
|
public String collectionFor(String typeId) {
|
||||||
|
String configured = elementCollections.get(typeId);
|
||||||
|
if (configured == null || configured.isBlank()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Keine Collection für Element-Typ '" + typeId + "' konfiguriert "
|
||||||
|
+ "(tasklist.element-collections).");
|
||||||
|
}
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package de.assecutor.tasklisteditor.document;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einstellungen eines einzelnen platzierten Elements (Block-Instanz) eines
|
||||||
|
* Canvas. Wird beim Speichern eines Canvas je Element-Typ in eine eigene
|
||||||
|
* MongoDB-Collection geschrieben (Collection-Name pro Typ konfigurierbar, siehe
|
||||||
|
* {@code tasklist.element-collections} bzw. die {@code MONGODB_ELEMENT_*}-
|
||||||
|
* Variablen in der {@code .env}).
|
||||||
|
*
|
||||||
|
* <p>Die Verknüpfung zum Canvas erfolgt über {@link #canvasId}; {@link #nodeId}
|
||||||
|
* identifiziert den Knoten innerhalb des Canvas.
|
||||||
|
*/
|
||||||
|
public class ElementSettingsDocument {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/** ID des zugehörigen {@link CanvasDocument}. */
|
||||||
|
private String canvasId;
|
||||||
|
|
||||||
|
/** Anzeigename des Canvas zum Zeitpunkt des Speicherns. */
|
||||||
|
private String canvasName;
|
||||||
|
|
||||||
|
/** Knoten-ID innerhalb des Canvas (Drawflow). */
|
||||||
|
private int nodeId;
|
||||||
|
|
||||||
|
/** Typ-ID des Elements (z. B. {@code text-liste-erfassen} oder {@code custom-1000}). */
|
||||||
|
private String typeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bei benutzerdefinierten Typen die Typ-ID des statischen Basistyps, in
|
||||||
|
* dessen Collection das Dokument abgelegt wird; bei festen Typen {@code null}.
|
||||||
|
*/
|
||||||
|
private String baseTypeId;
|
||||||
|
|
||||||
|
/** Bezeichnung des Tasks (entspricht {@code topic}). */
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
/** Erfasste Property-Werte des Elements. */
|
||||||
|
private Map<String, Object> values;
|
||||||
|
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
public ElementSettingsDocument() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElementSettingsDocument(String canvasId, String canvasName, int nodeId,
|
||||||
|
String typeId, String baseTypeId, String topic,
|
||||||
|
Map<String, Object> values, Instant createdAt) {
|
||||||
|
this.canvasId = canvasId;
|
||||||
|
this.canvasName = canvasName;
|
||||||
|
this.nodeId = nodeId;
|
||||||
|
this.typeId = typeId;
|
||||||
|
this.baseTypeId = baseTypeId;
|
||||||
|
this.topic = topic;
|
||||||
|
this.values = values;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCanvasId() {
|
||||||
|
return canvasId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCanvasId(String canvasId) {
|
||||||
|
this.canvasId = canvasId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCanvasName() {
|
||||||
|
return canvasName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCanvasName(String canvasName) {
|
||||||
|
this.canvasName = canvasName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNodeId() {
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNodeId(int nodeId) {
|
||||||
|
this.nodeId = nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTypeId() {
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeId(String typeId) {
|
||||||
|
this.typeId = typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBaseTypeId() {
|
||||||
|
return baseTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseTypeId(String baseTypeId) {
|
||||||
|
this.baseTypeId = baseTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTopic() {
|
||||||
|
return topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopic(String topic) {
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValues(Map<String, Object> values) {
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,22 @@ public record BlockType(
|
|||||||
int taskType,
|
int taskType,
|
||||||
int inputs,
|
int inputs,
|
||||||
int outputs,
|
int outputs,
|
||||||
List<PropertyDefinition> properties
|
List<PropertyDefinition> properties,
|
||||||
|
/** Verweis auf den statischen Basistyp (nur bei benutzerdefinierten Typen, sonst {@code null}). */
|
||||||
|
String baseTypeId
|
||||||
) {
|
) {
|
||||||
|
/** Konstruktor für feste Standard-Typen ohne Basistyp-Verweis. */
|
||||||
|
public BlockType(String id, String label, String description, String icon, String color,
|
||||||
|
int taskType, int inputs, int outputs, List<PropertyDefinition> properties) {
|
||||||
|
this(id, label, description, icon, color, taskType, inputs, outputs, properties, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rein virtuelle Blöcke (z. B. Start/Abschluss) dienen nur der
|
||||||
|
* Workflow-Darstellung im Canvas und werden nicht in die Task-Liste
|
||||||
|
* exportiert. Sie sind an einer negativen Typnummer erkennbar.
|
||||||
|
*/
|
||||||
|
public boolean isVirtual() {
|
||||||
|
return taskType < 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,39 @@ public class BlockTypeRegistry {
|
|||||||
|
|
||||||
private final Map<String, BlockType> types = new LinkedHashMap<>();
|
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) {
|
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(
|
register(new BlockType(
|
||||||
"ankunft-melden",
|
"ankunft-melden",
|
||||||
"Ankunft melden",
|
"Ankunft melden",
|
||||||
@@ -73,12 +105,12 @@ public class BlockTypeRegistry {
|
|||||||
// Fasst die früheren App-Typen Quittungsgeber, Bemerkung und
|
// Fasst die früheren App-Typen Quittungsgeber, Bemerkung und
|
||||||
// Kommissionsnummer zu einem einzigen Texterfassungs-Task zusammen.
|
// Kommissionsnummer zu einem einzigen Texterfassungs-Task zusammen.
|
||||||
register(new BlockType(
|
register(new BlockType(
|
||||||
"text-erfassen",
|
"quittungsgeber-erfassen",
|
||||||
"Text erfassen",
|
"Quittungsgeber erfassen",
|
||||||
"Erfasst einen Text per Eingabefeld (z. B. Name, Bemerkung oder Nummer).",
|
"Erfasst den Quittungsgeber per Eingabefeld.",
|
||||||
"vaadin:input",
|
"vaadin:input",
|
||||||
"#dc2626",
|
"#dc2626",
|
||||||
4,
|
7,
|
||||||
1, 1,
|
1, 1,
|
||||||
List.of(
|
List.of(
|
||||||
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
||||||
@@ -88,16 +120,30 @@ public class BlockTypeRegistry {
|
|||||||
));
|
));
|
||||||
|
|
||||||
register(new BlockType(
|
register(new BlockType(
|
||||||
"zahl-erfassen",
|
"bemerkung-erfassen",
|
||||||
"Zahl erfassen",
|
"Bemerkung erfassen",
|
||||||
"Erfasst einen numerischen Wert per Zahleneingabe.",
|
"Erfasst eine Bemerkung per Eingabefeld.",
|
||||||
"vaadin:abacus",
|
"vaadin:input",
|
||||||
"#0891b2",
|
"#dc2626",
|
||||||
5,
|
8,
|
||||||
1, 1,
|
1, 1,
|
||||||
List.of(
|
List.of(
|
||||||
PropertyDefinition.number("min", "Mindestwert", 0),
|
PropertyDefinition.number("max_len", "Maximale Zeichenanzahl (0 = unbegrenzt)", 0),
|
||||||
PropertyDefinition.number("max", "Maximalwert (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("opt", "Optionale Aufgabe", false),
|
||||||
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
PropertyDefinition.bool("reenter", "Erneut bearbeitbar", false)
|
||||||
)
|
)
|
||||||
@@ -123,7 +169,7 @@ public class BlockTypeRegistry {
|
|||||||
"Erfasst eine Liste gescannter Barcodes. (In der Flutter-App deaktiviert.)",
|
"Erfasst eine Liste gescannter Barcodes. (In der Flutter-App deaktiviert.)",
|
||||||
"vaadin:barcode",
|
"vaadin:barcode",
|
||||||
"#0d9488",
|
"#0d9488",
|
||||||
7,
|
5,
|
||||||
1, 1,
|
1, 1,
|
||||||
List.of(
|
List.of(
|
||||||
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
PropertyDefinition.bool("opt", "Optionale Aufgabe", false),
|
||||||
@@ -139,7 +185,7 @@ public class BlockTypeRegistry {
|
|||||||
"Lässt einen Status aus einer Auswahlliste wählen.",
|
"Lässt einen Status aus einer Auswahlliste wählen.",
|
||||||
"vaadin:list-select",
|
"vaadin:list-select",
|
||||||
"#ea580c",
|
"#ea580c",
|
||||||
8,
|
11,
|
||||||
1, 1,
|
1, 1,
|
||||||
List.of(
|
List.of(
|
||||||
PropertyDefinition.textArea("choices", "Statusoptionen (eine pro Zeile)"),
|
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) {
|
if (!hideScriptBlock) {
|
||||||
register(new BlockType(
|
register(new BlockType(
|
||||||
"script",
|
"script",
|
||||||
@@ -217,7 +312,8 @@ public class BlockTypeRegistry {
|
|||||||
taskType,
|
taskType,
|
||||||
base.inputs(),
|
base.inputs(),
|
||||||
base.outputs(),
|
base.outputs(),
|
||||||
List.copyOf(properties)
|
List.copyOf(properties),
|
||||||
|
base.id()
|
||||||
);
|
);
|
||||||
register(custom);
|
register(custom);
|
||||||
return custom;
|
return custom;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package de.assecutor.tasklisteditor.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einzelnes Eingabefeld eines Listen-Text-Tasks (Typ 12).
|
||||||
|
* Definiert Titel und Platzhalter eines vom Bearbeiter auszufüllenden
|
||||||
|
* Textfeldes.
|
||||||
|
*
|
||||||
|
* <p>{@code null}-Felder werden ausgelassen.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@JsonPropertyOrder({"title", "placeholder"})
|
||||||
|
public class FieldDto {
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
private String placeholder;
|
||||||
|
|
||||||
|
public FieldDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldDto(String title, String placeholder) {
|
||||||
|
this.title = title;
|
||||||
|
this.placeholder = placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlaceholder() {
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaceholder(String placeholder) {
|
||||||
|
this.placeholder = placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.assecutor.tasklisteditor.model;
|
package de.assecutor.tasklisteditor.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public record PropertyDefinition(String id, String label, PropertyType type, Object defaultValue) {
|
public record PropertyDefinition(String id, String label, PropertyType type, Object defaultValue) {
|
||||||
public static PropertyDefinition text(String id, String label) {
|
public static PropertyDefinition text(String id, String label) {
|
||||||
return new PropertyDefinition(id, label, PropertyType.TEXT, "");
|
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) {
|
public static PropertyDefinition bool(String id, String label, boolean defaultValue) {
|
||||||
return new PropertyDefinition(id, label, PropertyType.BOOLEAN, defaultValue);
|
return new PropertyDefinition(id, label, PropertyType.BOOLEAN, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PropertyDefinition fieldList(String id, String label) {
|
||||||
|
return new PropertyDefinition(id, label, PropertyType.FIELD_LIST, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyDefinition nameList(String id, String label) {
|
||||||
|
return new PropertyDefinition(id, label, PropertyType.NAME_LIST, List.of());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,9 @@ public enum PropertyType {
|
|||||||
TEXT,
|
TEXT,
|
||||||
TEXTAREA,
|
TEXTAREA,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
BOOLEAN
|
BOOLEAN,
|
||||||
|
/** Wiederholbare Liste aus Eingabefeldern mit Titel und Platzhalter. */
|
||||||
|
FIELD_LIST,
|
||||||
|
/** Wiederholbare Liste aus reinen Namens-Einträgen (ein Textfeld je Zeile). */
|
||||||
|
NAME_LIST
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@JsonPropertyOrder({"s_id", "type", "topic", "opt", "reenter", "min", "max",
|
@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 {
|
public class TaskDto {
|
||||||
|
|
||||||
@JsonProperty("s_id")
|
@JsonProperty("s_id")
|
||||||
@@ -50,6 +50,9 @@ public class TaskDto {
|
|||||||
/** Auswahloptionen eines Status-Tasks (Typ 6). */
|
/** Auswahloptionen eines Status-Tasks (Typ 6). */
|
||||||
private List<ChoiceDto> choices;
|
private List<ChoiceDto> choices;
|
||||||
|
|
||||||
|
/** Eingabefelder eines Listen-Text-Tasks (Typ 12). */
|
||||||
|
private List<FieldDto> fields;
|
||||||
|
|
||||||
/** Logik-Graph eines Script-Blocks (Drawflow-Definition als JSON). */
|
/** Logik-Graph eines Script-Blocks (Drawflow-Definition als JSON). */
|
||||||
private JsonNode script;
|
private JsonNode script;
|
||||||
|
|
||||||
@@ -173,6 +176,14 @@ public class TaskDto {
|
|||||||
this.choices = choices;
|
this.choices = choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FieldDto> getFields() {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFields(List<FieldDto> fields) {
|
||||||
|
this.fields = fields;
|
||||||
|
}
|
||||||
|
|
||||||
public JsonNode getScript() {
|
public JsonNode getScript() {
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ public class CustomTypeService {
|
|||||||
doc.getOutputs(),
|
doc.getOutputs(),
|
||||||
doc.getProperties() == null ? List.of()
|
doc.getProperties() == null ? List.of()
|
||||||
: doc.getProperties().stream()
|
: doc.getProperties().stream()
|
||||||
.map(CustomPropertyState::toDefinition)
|
.map(prop -> prop.toDefinition())
|
||||||
.toList()
|
.toList(),
|
||||||
|
doc.getBaseTypeId()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.assecutor.tasklisteditor.service;
|
||||||
|
|
||||||
|
import de.assecutor.tasklisteditor.config.ElementCollectionProperties;
|
||||||
|
import de.assecutor.tasklisteditor.document.ElementSettingsDocument;
|
||||||
|
import de.assecutor.tasklisteditor.model.BlockInstance;
|
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert die Einstellungen der einzelnen platzierten Elemente eines Canvas
|
||||||
|
* jeweils in einer eigenen, pro Element-Typ konfigurierten MongoDB-Collection.
|
||||||
|
*
|
||||||
|
* <p>Wird ergänzend zum {@link CanvasService} aufgerufen: Der Canvas-Stand
|
||||||
|
* (Layout + Block-Werte) bleibt unverändert im Canvas-Dokument; zusätzlich wird
|
||||||
|
* je Element ein {@link ElementSettingsDocument} in die Typ-Collection
|
||||||
|
* geschrieben und über die Canvas-ID verknüpft.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ElementSettingsService {
|
||||||
|
|
||||||
|
private final MongoTemplate mongoTemplate;
|
||||||
|
private final ElementCollectionProperties properties;
|
||||||
|
|
||||||
|
public ElementSettingsService(MongoTemplate mongoTemplate,
|
||||||
|
ElementCollectionProperties properties) {
|
||||||
|
this.mongoTemplate = mongoTemplate;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schreibt für jedes Element des Canvas ein Einstellungs-Dokument in die
|
||||||
|
* Collection seines Typs.
|
||||||
|
*
|
||||||
|
* @param canvasId ID des gespeicherten Canvas (Verknüpfung)
|
||||||
|
* @param canvasName Anzeigename des Canvas
|
||||||
|
* @param instances platzierte Block-Instanzen
|
||||||
|
*/
|
||||||
|
public void saveForCanvas(String canvasId, String canvasName,
|
||||||
|
Collection<BlockInstance> instances) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
for (BlockInstance instance : instances) {
|
||||||
|
// Virtuelle Blöcke (Start/Abschluss) haben keine fachlichen
|
||||||
|
// Einstellungen und keine Collection – sie werden nicht persistiert.
|
||||||
|
if (instance.type().isVirtual()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String typeId = instance.type().id();
|
||||||
|
String baseTypeId = instance.type().baseTypeId();
|
||||||
|
// Benutzerdefinierte Typen landen in der Collection ihres statischen
|
||||||
|
// Basistyps; feste Typen (baseTypeId == null) in ihrer eigenen.
|
||||||
|
String collectionTypeId = (baseTypeId == null || baseTypeId.isBlank())
|
||||||
|
? typeId
|
||||||
|
: baseTypeId;
|
||||||
|
ElementSettingsDocument document = new ElementSettingsDocument(
|
||||||
|
canvasId,
|
||||||
|
canvasName,
|
||||||
|
instance.id(),
|
||||||
|
typeId,
|
||||||
|
baseTypeId,
|
||||||
|
instance.topic(),
|
||||||
|
new LinkedHashMap<>(instance.values()),
|
||||||
|
now);
|
||||||
|
mongoTemplate.save(document, properties.collectionFor(collectionTypeId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ package de.assecutor.tasklisteditor.service;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.assecutor.tasklisteditor.model.BlockInstance;
|
import de.assecutor.tasklisteditor.model.BlockInstance;
|
||||||
|
import de.assecutor.tasklisteditor.model.BlockTypeRegistry;
|
||||||
import de.assecutor.tasklisteditor.model.ChoiceDto;
|
import de.assecutor.tasklisteditor.model.ChoiceDto;
|
||||||
|
import de.assecutor.tasklisteditor.model.FieldDto;
|
||||||
import de.assecutor.tasklisteditor.model.TaskDto;
|
import de.assecutor.tasklisteditor.model.TaskDto;
|
||||||
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -47,6 +49,10 @@ public class TaskListExporter {
|
|||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException("Editor-Graph konnte nicht gelesen werden: " + ex.getMessage(), 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()) {
|
if (!data.isObject() || data.isEmpty()) {
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
@@ -59,6 +65,11 @@ public class TaskListExporter {
|
|||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
continue;
|
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);
|
TaskDto task = toTask(instance);
|
||||||
task.setSId(sId++);
|
task.setSId(sId++);
|
||||||
tasks.add(task);
|
tasks.add(task);
|
||||||
@@ -66,6 +77,64 @@ public class TaskListExporter {
|
|||||||
return tasks;
|
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) {
|
private List<Integer> determineOrder(JsonNode data) {
|
||||||
// Knoten einsammeln + eingehende Verbindungen zählen.
|
// Knoten einsammeln + eingehende Verbindungen zählen.
|
||||||
List<Integer> nodeIds = new ArrayList<>();
|
List<Integer> nodeIds = new ArrayList<>();
|
||||||
@@ -144,13 +213,20 @@ public class TaskListExporter {
|
|||||||
|
|
||||||
if (v.containsKey("entries")) {
|
if (v.containsKey("entries")) {
|
||||||
boolean queryRemark = v.containsKey("query_remark") && asBool(v.get("query_remark"));
|
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")) {
|
if (v.containsKey("choices")) {
|
||||||
task.setChoices(parseChoices(asText(v.get("choices"))));
|
task.setChoices(parseChoices(asText(v.get("choices"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (v.containsKey("fields")) {
|
||||||
|
task.setFields(parseFields(v.get("fields")));
|
||||||
|
}
|
||||||
|
|
||||||
if (v.containsKey("script")) {
|
if (v.containsKey("script")) {
|
||||||
String script = asText(v.get("script"));
|
String script = asText(v.get("script"));
|
||||||
if (script != null) {
|
if (script != null) {
|
||||||
@@ -179,6 +255,21 @@ public class TaskListExporter {
|
|||||||
return entries.isEmpty() ? null : entries;
|
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) {
|
private List<ChoiceDto> parseChoices(String raw) {
|
||||||
if (raw == null || raw.isBlank()) {
|
if (raw == null || raw.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -194,6 +285,23 @@ public class TaskListExporter {
|
|||||||
return choices.isEmpty() ? null : choices;
|
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) {
|
private Boolean asBool(Object o) {
|
||||||
return o instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(o));
|
return o instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(o));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package de.assecutor.tasklisteditor.service;
|
|||||||
|
|
||||||
import de.assecutor.tasklisteditor.model.BlockType;
|
import de.assecutor.tasklisteditor.model.BlockType;
|
||||||
import de.assecutor.tasklisteditor.model.BlockTypeRegistry;
|
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.PropertyDefinition;
|
||||||
import de.assecutor.tasklisteditor.model.TaskDto;
|
import de.assecutor.tasklisteditor.model.TaskDto;
|
||||||
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
import de.assecutor.tasklisteditor.model.TaskEntryDto;
|
||||||
@@ -63,7 +63,21 @@ public class TaskListImporter {
|
|||||||
private Map<String, Object> valuesFor(BlockType type, TaskDto task) {
|
private Map<String, Object> valuesFor(BlockType type, TaskDto task) {
|
||||||
Map<String, Object> values = new LinkedHashMap<>();
|
Map<String, Object> values = new LinkedHashMap<>();
|
||||||
for (PropertyDefinition def : type.properties()) {
|
for (PropertyDefinition def : type.properties()) {
|
||||||
Object value = switch (def.id()) {
|
// Listen-Eigenschaften (Typ 12–14) werden anhand ihres
|
||||||
|
// Property-Typs in die vom Zeilen-Editor erwartete Struktur
|
||||||
|
// (Liste aus {title}/{title,placeholder}-Maps) zurückgewandelt.
|
||||||
|
Object value = switch (def.type()) {
|
||||||
|
case FIELD_LIST -> fieldRows(task);
|
||||||
|
case NAME_LIST -> entryRows(task);
|
||||||
|
default -> scalarValue(def, task);
|
||||||
|
};
|
||||||
|
values.put(def.id(), value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object scalarValue(PropertyDefinition def, TaskDto task) {
|
||||||
|
return switch (def.id()) {
|
||||||
case "opt" -> orDefault(task.getOpt(), def.defaultValue());
|
case "opt" -> orDefault(task.getOpt(), def.defaultValue());
|
||||||
case "reenter" -> orDefault(task.getReenter(), def.defaultValue());
|
case "reenter" -> orDefault(task.getReenter(), def.defaultValue());
|
||||||
case "rt" -> orDefault(task.getRt(), def.defaultValue());
|
case "rt" -> orDefault(task.getRt(), def.defaultValue());
|
||||||
@@ -77,9 +91,35 @@ public class TaskListImporter {
|
|||||||
case "choices" -> choicesText(task);
|
case "choices" -> choicesText(task);
|
||||||
default -> def.defaultValue();
|
default -> def.defaultValue();
|
||||||
};
|
};
|
||||||
values.put(def.id(), value);
|
|
||||||
}
|
}
|
||||||
return values;
|
|
||||||
|
/** {@code fields}-Array (Typ 12/13) zurück in Zeilen mit Titel + Platzhalter. */
|
||||||
|
private List<Map<String, Object>> fieldRows(TaskDto task) {
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
if (task.getFields() == null) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
for (FieldDto field : task.getFields()) {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("title", field.getTitle() == null ? "" : field.getTitle());
|
||||||
|
row.put("placeholder", field.getPlaceholder() == null ? "" : field.getPlaceholder());
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code entries}-Array (Typ 14) zurück in Zeilen mit Namen ({@code title}). */
|
||||||
|
private List<Map<String, Object>> entryRows(TaskDto task) {
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
if (task.getEntries() == null) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
for (TaskEntryDto entry : task.getEntries()) {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("title", entry.getText() == null ? "" : entry.getText());
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object orDefault(Object value, Object fallback) {
|
private Object orDefault(Object value, Object fallback) {
|
||||||
@@ -88,7 +128,7 @@ public class TaskListImporter {
|
|||||||
|
|
||||||
private boolean anyQueryRemark(TaskDto task) {
|
private boolean anyQueryRemark(TaskDto task) {
|
||||||
return task.getEntries() != null
|
return task.getEntries() != null
|
||||||
&& task.getEntries().stream().anyMatch(TaskEntryDto::isQueryRemark);
|
&& task.getEntries().stream().anyMatch(entry -> entry.isQueryRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String entriesText(TaskDto task) {
|
private String entriesText(TaskDto task) {
|
||||||
@@ -96,14 +136,14 @@ public class TaskListImporter {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return task.getEntries().stream()
|
return task.getEntries().stream()
|
||||||
.map(TaskEntryDto::getText)
|
.map(entry -> entry.getText())
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String choicesText(TaskDto task) {
|
private String choicesText(TaskDto task) {
|
||||||
if (task.getChoices() != null && !task.getChoices().isEmpty()) {
|
if (task.getChoices() != null && !task.getChoices().isEmpty()) {
|
||||||
return task.getChoices().stream()
|
return task.getChoices().stream()
|
||||||
.map(ChoiceDto::getTxt)
|
.map(choice -> choice.getTxt())
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
// Altdaten des früheren Typ 11 (Abhol-/Lieferstatus) liefern die Optionen
|
// Altdaten des früheren Typ 11 (Abhol-/Lieferstatus) liefern die Optionen
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ public class TaskListService {
|
|||||||
this.objectMapper = objectMapper;
|
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. */
|
/** Ergebnis eines Exports: typisierte Tasks plus formatiertes JSON. */
|
||||||
public record ExportResult(List<TaskDto> tasks, String json) {
|
public record ExportResult(List<TaskDto> tasks, String json) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import de.assecutor.tasklisteditor.model.TaskListPayload;
|
|||||||
import de.assecutor.tasklisteditor.document.CanvasDocument;
|
import de.assecutor.tasklisteditor.document.CanvasDocument;
|
||||||
import de.assecutor.tasklisteditor.service.CanvasService;
|
import de.assecutor.tasklisteditor.service.CanvasService;
|
||||||
import de.assecutor.tasklisteditor.service.CustomTypeService;
|
import de.assecutor.tasklisteditor.service.CustomTypeService;
|
||||||
|
import de.assecutor.tasklisteditor.service.ElementSettingsService;
|
||||||
import de.assecutor.tasklisteditor.service.PendingImportStore;
|
import de.assecutor.tasklisteditor.service.PendingImportStore;
|
||||||
import de.assecutor.tasklisteditor.service.TaskListImporter;
|
import de.assecutor.tasklisteditor.service.TaskListImporter;
|
||||||
import de.assecutor.tasklisteditor.service.TaskListService;
|
import de.assecutor.tasklisteditor.service.TaskListService;
|
||||||
@@ -72,6 +73,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
private final PendingImportStore importStore;
|
private final PendingImportStore importStore;
|
||||||
private final CanvasService canvasService;
|
private final CanvasService canvasService;
|
||||||
private final CustomTypeService customTypeService;
|
private final CustomTypeService customTypeService;
|
||||||
|
private final ElementSettingsService elementSettingsService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final NodeEditor nodeEditor;
|
private final NodeEditor nodeEditor;
|
||||||
private final Map<Integer, BlockInstance> instances = new HashMap<>();
|
private final Map<Integer, BlockInstance> instances = new HashMap<>();
|
||||||
@@ -85,6 +87,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
PendingImportStore importStore,
|
PendingImportStore importStore,
|
||||||
CanvasService canvasService,
|
CanvasService canvasService,
|
||||||
CustomTypeService customTypeService,
|
CustomTypeService customTypeService,
|
||||||
|
ElementSettingsService elementSettingsService,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.taskListService = taskListService;
|
this.taskListService = taskListService;
|
||||||
@@ -92,6 +95,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
this.importStore = importStore;
|
this.importStore = importStore;
|
||||||
this.canvasService = canvasService;
|
this.canvasService = canvasService;
|
||||||
this.customTypeService = customTypeService;
|
this.customTypeService = customTypeService;
|
||||||
|
this.elementSettingsService = elementSettingsService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
setPadding(false);
|
setPadding(false);
|
||||||
@@ -432,6 +436,10 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
* Canvas-Graphen ermittelt, um sie als Variablen anzubieten.
|
* Canvas-Graphen ermittelt, um sie als Variablen anzubieten.
|
||||||
*/
|
*/
|
||||||
private void openSettingsDialog(BlockInstance instance) {
|
private void openSettingsDialog(BlockInstance instance) {
|
||||||
|
// Rein virtuelle Elemente (Start/Abschluss) haben keine Einstellungen.
|
||||||
|
if (instance.type().isVirtual()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ("script".equals(instance.type().id())) {
|
if ("script".equals(instance.type().id())) {
|
||||||
nodeEditor.exportGraph(graphJson -> openScriptDialog(instance, graphJson));
|
nodeEditor.exportGraph(graphJson -> openScriptDialog(instance, graphJson));
|
||||||
return;
|
return;
|
||||||
@@ -712,10 +720,111 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
box.addValueChangeListener(e -> instance.set(def.id(), e.getValue()));
|
box.addValueChangeListener(e -> instance.set(def.id(), e.getValue()));
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
case FIELD_LIST -> {
|
||||||
|
return buildRowList(instance, def, current, true);
|
||||||
|
}
|
||||||
|
case NAME_LIST -> {
|
||||||
|
return buildRowList(instance, def, current, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Span();
|
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
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
List<String> tokens = event.getLocation().getQueryParameters()
|
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 -> {
|
Button save = new Button("Speichern", new Icon(VaadinIcon.HARDDRIVE), ev -> {
|
||||||
try {
|
try {
|
||||||
String id = canvasService.save(name.getValue(), layout, collectBlocks());
|
String id = canvasService.save(name.getValue(), layout, collectBlocks());
|
||||||
|
elementSettingsService.saveForCanvas(id, name.getValue(), instances.values());
|
||||||
currentListName = name.getValue();
|
currentListName = name.getValue();
|
||||||
Notification.show("Canvas gespeichert (ID: " + id + ")");
|
Notification.show("Canvas gespeichert (ID: " + id + ")");
|
||||||
dialog.close();
|
dialog.close();
|
||||||
@@ -840,7 +950,7 @@ public class MainView extends VerticalLayout implements BeforeEnterObserver {
|
|||||||
.withZone(ZoneId.systemDefault());
|
.withZone(ZoneId.systemDefault());
|
||||||
|
|
||||||
Grid<CanvasDocument> grid = new Grid<>();
|
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.getBlocks() == null ? 0 : d.getBlocks().size()).setHeader("Blöcke");
|
||||||
grid.addColumn(d -> d.getCreatedAt() == null ? "" : fmt.format(d.getCreatedAt()))
|
grid.addColumn(d -> d.getCreatedAt() == null ? "" : fmt.format(d.getCreatedAt()))
|
||||||
.setHeader("Gespeichert");
|
.setHeader("Gespeichert");
|
||||||
|
|||||||
@@ -25,3 +25,22 @@ management.health.mongo.enabled=false
|
|||||||
# Script-Block (grafisches Logik-Element) im Editor ausblenden.
|
# Script-Block (grafisches Logik-Element) im Editor ausblenden.
|
||||||
# Wird per .env (HIDE_SCRIPT_BLOCK) gesteuert.
|
# Wird per .env (HIDE_SCRIPT_BLOCK) gesteuert.
|
||||||
tasklist.hide-script-block=${HIDE_SCRIPT_BLOCK:false}
|
tasklist.hide-script-block=${HIDE_SCRIPT_BLOCK:false}
|
||||||
|
|
||||||
|
# --- Collections je Element-Typ ---------------------------------------------
|
||||||
|
# Einstellungen der einzelnen Elemente werden beim Canvas-Speichern zusätzlich
|
||||||
|
# je Element-Typ in eine eigene Collection geschrieben. Namen aus der .env
|
||||||
|
# (MONGODB_ELEMENT_*), mit Default. Benutzerdefinierte Typen werden auf ihren
|
||||||
|
# statischen Basistyp aufgelöst; für jeden Typ muss eine Zuordnung existieren.
|
||||||
|
tasklist.element-collections[ankunft-melden]=${MONGODB_ELEMENT_ARRIVAL_COLLECTION:element_arrival}
|
||||||
|
tasklist.element-collections[unterschrift-erfassen]=${MONGODB_ELEMENT_SIGNATURE_COLLECTION:element_signature}
|
||||||
|
tasklist.element-collections[fotos-aufnehmen]=${MONGODB_ELEMENT_PHOTOS_COLLECTION:element_photos}
|
||||||
|
tasklist.element-collections[quittungsgeber-erfassen]=${MONGODB_ELEMENT_RECEIPT_GIVER_COLLECTION:element_receipt_giver}
|
||||||
|
tasklist.element-collections[bemerkung-erfassen]=${MONGODB_ELEMENT_REMARK_COLLECTION:element_remark}
|
||||||
|
tasklist.element-collections[kommissionsnummer-erfassen]=${MONGODB_ELEMENT_COMMISSION_NUMBER_COLLECTION:element_commission_number}
|
||||||
|
tasklist.element-collections[checkbox]=${MONGODB_ELEMENT_CHECKBOX_COLLECTION:element_checkbox}
|
||||||
|
tasklist.element-collections[barcode-scannen]=${MONGODB_ELEMENT_BARCODE_COLLECTION:element_barcode}
|
||||||
|
tasklist.element-collections[statusauswahl]=${MONGODB_ELEMENT_STATUS_SELECTION_COLLECTION:element_status_selection}
|
||||||
|
tasklist.element-collections[text-liste-erfassen]=${MONGODB_ELEMENT_TEXT_LIST_COLLECTION:element_text_list}
|
||||||
|
tasklist.element-collections[zahl-liste-erfassen]=${MONGODB_ELEMENT_NUMBER_LIST_COLLECTION:element_number_list}
|
||||||
|
tasklist.element-collections[checkbox-liste-erfassen]=${MONGODB_ELEMENT_CHECKBOX_LIST_COLLECTION:element_checkbox_list}
|
||||||
|
tasklist.element-collections[script]=${MONGODB_ELEMENT_SCRIPT_COLLECTION:element_script}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"ignoreDeprecations": "5.0",
|
"ignoreDeprecations": "6.0",
|
||||||
"baseUrl": "src/main/frontend",
|
"baseUrl": "src/main/frontend",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||||
|
|||||||
Reference in New Issue
Block a user