Skip to content

intent: a list column may show a record's child rows - list: [skills.skill] renders the children's texts as chips (#8295) - #8303

Merged
iliyan-velichkov merged 1 commit into
masterfrom
issue-8295-list-collection-column
Oct 28, 2026
Merged

iliyan-velichkov merged 1 commit into
masterfrom
issue-8295-list-collection-column

Conversation

@iliyan-velichkov

Copy link
Copy Markdown
Contributor

Cause

A record's children were invisible in every generated list. list: accepted a field, a to-one or subset relation, or a label's Name, and refused a oneToMany ("has no column on the entity"), so the reader had to open each row to learn what it held.

Change

Implemented as the issue's decisions describe, with one deviation in how a to-one's text is read (below).

Syntax (parser). A list: entry may be <collection>.<property> or a bare <collection> (= <collection>.Name, which needs the child's label:). A manyToMany arrives as the oneToMany to its link entity. The parser refuses each of the following with a message naming the token:

  • an unknown collection or property;
  • a path deeper than one hop;
  • a property that is itself a collection;
  • a bare collection whose child has no label;
  • a child that does not reference the entity through exactly one to-one;
  • a column name that collides with a declared property.

A label: on the collection used to be refused ("renders no control of its own"). It is now accepted once the list shows the collection, and it heads the column.

Model. EdmIntentGenerator.buildCollectionColumns emits one collectionColumns entry per path. It is a structured attribute, so it survives the .edm. Each entry carries the child entity and its perspective, the child's FK back, the property shown and, for a to-one, its target's display property. The column's name (SkillsSkill, or Skills for a bare collection) stands in listOrder where the author wrote it. ModelParameterProcessor then:

  • places the column in listProperties and in a register's columns;
  • resolves the child's and the to-one target's generated classes;
  • gives the reads the child's order: its orderBy:, else its primary key.

Server. The controller's list, POST /search and GET /{id} fill a @Transient List<String> on the entity, once per page and never once per row: the child's repository by Criteria.in(<fk>, keys), then for a to-one the target's repository by the distinct target keys. Because the field is transient, a write that carries it changes nothing.

Deviation from decision 2: the issue says "one query, joined to the to-one's target". I read through the two repositories instead (two reads per page for a to-one, still none per row), for two reasons:

  • a raw join would bypass the repositories' translation overlay, so a multilingual target would show base-language text where its dropdown shows the translated one;
  • JavaRepository has no projection query.

For the same reason, no new method was needed in Repository.java.template.

UI. The column renders as chips in the power list (no sort; filter type none, so it is not offered as a filter), in a composition child's detail panel, and in a related: register without show:. The export writes ", "-joined text. The header is the collection's label, with an en-US catalog entry. The panel tables are markup shared by every model, so their chip branch is gated on rendersCollectionColumns, which is set only for a model that renders a collection column anywhere. detailPanel.cellValue also joins a list column for any text context. The my and partner lists and the form/document summary lines leave the column out, as scoped.

Verification

Run and green:

  • ListColumnsIntentTest: aCollectionPathIsAListColumn, aBareCollectionNeedsTheChildsLabel, anUnknownOrDeeperPathIsRefused.
  • EdmIntentGeneratorTest.aCollectionPathEmitsACollectionColumn: the emitted column, its listOrder position, and a register reproducing it.
  • ModelParameterProcessorTest.aCollectionColumnIsListedAndResolvedForTheController.
  • Full module suites: engine-intent 1884, ide-template 318.
  • IntentEmissionCoverageIT with the issue's model generated, compiled and running:
    • the master-filtered list, a plain page, /search and GET /{id} answer SkillsSkill: ["Java","SQL"] for ann and [] for a member without skills;
    • SkillsGrantedBy answers the field's values;
    • a PUT carrying SkillsSkill: ["COBOL"] leaves the skills as they were.
  • CollectionColumnHarmoniaIT (new, browser): ann's row shows the chips Java and SQL in the Team page's TeamMember panel. The same chips show in a root entity's power list and in a register on another entity's page. The issue's TeamMember is a composition child, which gets no power list page of its own, so the test adds an Employee/Department pair for the list and the register.
  • mvn -T 1C formatter:validate with the formatter cache wiped.
  • Release-profile javadoc on engine-intent and ide-template.

Not verified:

  • "A model without collection paths generates byte-identical output" holds by construction: every changed template line is gated on attributes such a model never carries. I did not diff generated output against master.
  • The .test manifest's list now names the collection column (it is the .model's listOrder). I could not check how the external app-test SDK's list flow treats a name that is not a field.
  • PostgreSQL was not run locally.

Fixes #8295

🤖 Generated with Claude Code

…skill] renders the children's texts as chips (#8295)

A record's children were invisible in every generated list: list: accepted a
field, a to-one or subset relation, or a label's Name, and refused a oneToMany
("has no column on the entity"), so the reader opened each row to learn what
it held.

A list: entry may now be a collection path, <collection>.<property> - a
oneToMany of the entity (a manyToMany arrives as the oneToMany to its link
entity) and a field or to-one of its child - or a bare <collection>, meaning
<collection>.Name, which needs the child's label:. The parser refuses, naming
the token, an unknown collection or property, a path deeper than one hop, a
property that is itself a collection, a bare collection without the child's
label, a child not referencing the entity through exactly one to-one, and a
column name colliding with a declared property. A label: on the collection
is accepted once the list shows it, and heads the column.

EdmIntentGenerator emits one collectionColumns entry per path (a structured
attribute, so it survives the .edm), its name (SkillsSkill / Skills) standing
in listOrder where the author wrote it; ModelParameterProcessor places it in
listProperties and a register's columns, and resolves the child's and the
to-one target's generated classes and the child's order. The generated
controller's list, POST /search and GET /{id} fill a @transient List<String>
on the entity per page - the child's repository by Criteria.in(<fk>, keys),
then a to-one's targets by their keys - never a read per row. It reads through
the repositories rather than a raw join so a multilingual child or target is
translated, as its dropdown is; transient, a write carrying it changes nothing.

The power list, a composition child's detail panel and a register without
show: render the texts as chips (no sort, no filter); the export joins them
with ", ". The panel tables are markup shared by every model, so their chip
branch is gated on rendersCollectionColumns, set only for a model that renders
a collection column: a model without one generates as before. The my/partner
lists and the form/document summary lines leave the column out.

Verified: ListColumnsIntentTest, EdmIntentGeneratorTest,
ModelParameterProcessorTest (engine-intent 1884, ide-template 318 green);
IntentEmissionCoverageIT (list, search, get, a PUT carrying the column) and the
new CollectionColumnHarmoniaIT (chips in the panel, the power list and the
register) green; formatter:validate with the cache wiped; release javadoc on
engine-intent and ide-template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov self-assigned this Oct 28, 2026
@iliyan-velichkov
iliyan-velichkov merged commit b7eaa23 into master Oct 28, 2026
16 checks passed
@iliyan-velichkov
iliyan-velichkov deleted the issue-8295-list-collection-column branch October 28, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: a list column may show a record's child rows - list: [skills.skill] renders the children's texts as chips

1 participant