Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,12 @@ private static List<Map<String, Object>> 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
// __<Fk>EntityUrl / __<Fk>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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> form = FormIntentGenerator.buildFormsForTest(IntentParser.parse(yaml))
.get("IdentifyDriver");
@SuppressWarnings("unchecked")
List<Map<String, Object>> controls = (List<Map<String, Object>>) 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down
Loading