From 014eec11f6b3a95186f7f568efbea0d99ba206d5 Mon Sep 17 00:00:00 2001 From: delchev Date: Thu, 20 Aug 2026 12:15:17 +0300 Subject: [PATCH] fix(templates): a moved document line resums the vacated document from the full-row paths too, and the relation picker stays a task-form control (#6863, #6866) Two residues of the 2026-08-19 wave, found in its review: - #6840 closed the moved-line hole for the TARGETED write paths; the full-row update() and updateWithoutEvent still resummed only the document the line points at NOW. A REST PUT re-pointing a line's FK - the most ordinary write there is - left the vacated document displaying, printing and POSTING a total that did not equal the sum of its lines. Both paths now read the before-FK through the BASE find (the history before-image's convention) and resum both documents when the line moved. Closes #6863. - The relation picker (#6849) locates its option list through the __EntityUrl / __EntityLabel process variables the trigger seeds, which exist only on a task form - but the generator's branch was reachable from a NON-task form listing a bare to-one relation in fields:, where isReadonlyByDefault(null) reads false and an editable, permanently-empty select replaced the text control that at least showed the raw key. The picker branch is now gated on the task form, with a fixture-surgery-guarded regression test. Closes #6866. Verified by ModelGenerationIT (render + blank-line guard), IntentEngineIT and IntentEmissionCoverageIT, plus the engine-intent unit suite. Co-Authored-By: Claude Fable 5 --- .../generator/form/FormIntentGenerator.java | 7 ++++- .../form/TaskFormRelationPickerTest.java | 28 +++++++++++++++++++ .../data/Repository.java.template | 22 +++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java index 9dd5eeec5ac..b237e1eef71 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java @@ -413,7 +413,12 @@ private static List> buildControls(FormIntent form, EntityIn // Task-form fields are read-only unless explicitly opted in via `editable`; other forms // keep the legacy "editable except a generated PK" behavior. boolean readonly = isTaskForm ? !editable.contains(fieldName) : isReadonlyByDefault(fieldsByName.get(fieldName)); - RelationIntent relation = fieldsByName.containsKey(fieldName) || entity == null ? null : toOneRelation(entity, fieldName); + // The picker is a TASK-FORM control: it locates its option list through the + // __EntityUrl / __EntityLabel process variables the trigger seeds, which exist + // only there. On a non-task form a to-one relation keeps the plain text control - the + // picker branch would render an editable select that stays permanently empty. + RelationIntent relation = + !isTaskForm || fieldsByName.containsKey(fieldName) || entity == null ? null : toOneRelation(entity, fieldName); if (relation != null && !readonly) { controls.add(relationPickerControl(relation, entitiesByName)); } else { diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/form/TaskFormRelationPickerTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/form/TaskFormRelationPickerTest.java index 3702fd1bc88..95a708743af 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/form/TaskFormRelationPickerTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/form/TaskFormRelationPickerTest.java @@ -109,6 +109,34 @@ void aDisplayedButNotEditableRelationIsNotAPicker() { assertEquals(Boolean.TRUE, control.get("readonly")); } + /** + * The picker is a TASK-FORM control: it locates its options through the process variables the + * trigger seeds, which exist only there. A non-task form listing a bare to-one relation keeps the + * plain text control - the picker branch would render an editable select that stays permanently + * empty, where a text control at least shows the raw key. + */ + @Test + void aRelationOnANonTaskFormNeverBecomesAPicker() { + String yaml = YAML.replace(""" + processes: + - name: Identify + trigger: { onCreate: Fine } + steps: + - { name: identify, kind: userTask, args: { assignee: officer, form: IdentifyDriver } } + - { name: done, kind: end } + """, "") + .replace("editable: [driver]\n actions: [identify]", ""); + assertTrue(!yaml.contains("processes:") && !yaml.contains("editable:"), + "the fixture surgery must actually detach the form from any process, or this test passes vacuously"); + Map form = FormIntentGenerator.buildFormsForTest(IntentParser.parse(yaml)) + .get("IdentifyDriver"); + @SuppressWarnings("unchecked") + List> controls = (List>) form.get("form"); + assertTrue(controls.stream() + .noneMatch(c -> "input-select".equals(c.get("controlId"))), + "a non-task form has no process context to feed a picker: " + controls); + } + /** * The FK is the target's integer key, so it rides the Writer's existing integer branch - a relation * adds no new coercion category. diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template index 77fdd71ba97..5f3a4a00e49 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template @@ -341,6 +341,14 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #if($documentChecks && $documentChecks.size() > 0) enforceChecks(entity); #end +#if($documentItem) + // The document this line belongs to BEFORE the write: a full-row update may re-point the + // line's FK to another document, and the one it leaves has no write of its own to resum it - + // the same moved-line hole the targeted path already closes. Read through the BASE find, like + // the history before-image below. + ${name}Entity documentBeforeRow = entity.${pkPropertyName} == null ? null : super.findById(entity.${pkPropertyName}); + Object documentBefore = documentBeforeRow == null ? null : documentBeforeRow.${documentItem.fkProperty}; +#end #if($history == "true") // Change history: the before-image is read through the BASE find (super), never this class's // overridden one - on a multilingual entity the overridden read overlays the caller's language, @@ -369,6 +377,10 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #if($documentItem) // Keep the document consistent: synchronously recompute the master's totals (no async roll-up). new ${documentItem.parentEntity}Repository().recalculate(updated.${documentItem.fkProperty}); + if (documentBefore != null && !documentBefore.equals(updated.${documentItem.fkProperty})) { + // The line MOVED: resum the document it left, which nothing else is going to touch. + new ${documentItem.parentEntity}Repository().recalculate(documentBefore); + } #end return updated; } @@ -395,6 +407,12 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { // the gate check makes an unbalanced document fail the transition, not silently post. enforceChecks(entity); #end +#if($documentItem) + // The document this line belongs to BEFORE the write - a system write can re-point the FK + // exactly as a user edit does, and the vacated document has no write of its own to resum it. + ${name}Entity documentBeforeRow = entity.${pkPropertyName} == null ? null : super.findById(entity.${pkPropertyName}); + Object documentBefore = documentBeforeRow == null ? null : documentBeforeRow.${documentItem.fkProperty}; +#end #if($history == "true") ${name}Entity historyBefore = entity.${pkPropertyName} == null ? null : super.findById(entity.${pkPropertyName}); ${name}Entity updated = super.update(entity); @@ -408,6 +426,10 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { // A system write is still a line change: the event is suppressed here, the arithmetic is not. // Keep the document consistent exactly as update() does (no async roll-up). new ${documentItem.parentEntity}Repository().recalculate(updated.${documentItem.fkProperty}); + if (documentBefore != null && !documentBefore.equals(updated.${documentItem.fkProperty})) { + // The line MOVED: resum the document it left, which nothing else is going to touch. + new ${documentItem.parentEntity}Repository().recalculate(documentBefore); + } #end return updated; }