Summary
Relation labels (<field>__display, #32) work for the common case, a to-one or to-many relation whose target's @display names a scalar field, shown in a column cell or on the detail page. Two other shapes show ids instead of names:
- A relation shown in the list's
primary column. The primary cell always renders the raw value through formatFieldValue, so a relation there shows the target id even though the API returned <field>__display. A relation becomes primary through an explicit @list(primary), and also automatically when the schema's own @display names a relation field. That is the natural choice for a join entity.
- A target whose
@display names a relation field. The server renders the display value with DynamicValue's Display impl, so __display is the literal string ref(<id>). The generated detail and list pages then show ref(product_01...). The edit-form RelationSelect for such a target labels each option with the raw id. This affects every relation that points at such a schema, including derived inverse collections (-> X[]), which is where it typically shows up.
Version checked
v0.45.0 release binary (x86_64 Linux, PostgreSQL build), source at tag v0.45.0 (10ec403), PostgreSQL 16.
Reproduction
@display("name")
schema Product {
name: text required
}
@display("title")
schema Order {
title: text required
lines: -> OrderLine[]
}
@display("product")
schema OrderLine {
order: -> Order required
product: -> Product required
}
Create a Product "Blue widget", an Order "First order" and an OrderLine pointing at both, then read them (GET .../entities/{id} and list give the same result):
GET .../OrderLine/entities/<id>
{"order":"order_01...","product":"product_01...","order__display":"First order","product__display":"Blue widget"}
GET .../Order/entities/<id>
{"lines":["orderline_01..."],"title":"First order","lines__display":["ref(product_01m3d41b6yevprsbadszjnera2)"]}
Generated site (schemaforge site generate -s schemas -o app):
/app/order-line: @display("product") promotes product to the primary column. The generated cell is formatFieldValue(row.original.product, { kind: "relation_one" }), which renders product_01..., although product__display is "Blue widget".
/app/order/<id>: the Lines row shows a link labelled ref(product_01m3d41b6yevprsbadszjnera2).
- Any edit form with a
-> OrderLine picker labels each option product_01....
Shapes that work: order/product as @list(column) cells and on the OrderLine detail page, and stored or derived -> X[] where X's @display is a scalar.
Expected
-
A relation in the primary column shows its __display label, as a column cell does, while still linking to the row's own detail page.
-
For a target whose @display is a relation, either:
- the server resolves one more hop, so
lines__display is ["Blue widget"]; or
- the DSL rejects
@display on a relation field (or on anything that is not a scalar) at parse time, so the problem cannot be declared.
At the very least, __display should never be the internal ref(...) rendering.
Source
crates/schema-forge-cli/templates/site/src/app/pages/list.generated.tsx.jinja:78-101: the primary branch handles enum specially and sends everything else, relations included, to formatFieldValue. The column branch (:106) has, at :114-143, the relation_one / relation_many + relation_display_field cases that read __display.
crates/schema-forge-cli/src/commands/site/context.rs:195-209: @display("<field>") auto-promotes that field to primary, even when it is a relation.
crates/schema-forge-acton/src/routes/entities.rs:1875-1876: display_value_to_string(display_value) on the target's display field. :2397-2405: non-scalar values fall back to other.to_string().
crates/schema-forge-core/src/types/dynamic_value.rs:76: Self::Ref(id) => write!(f, "ref({id})").
crates/schema-forge-core/src/types/schema_definition.rs:119-124 and the @display parser (crates/schema-forge-dsl/src/parser.rs:433): no check on the named field's type.
labelFor in the vendored src/components/ui/relation-select.tsx (crates/schema-forge-cli/src/commands/site/vendor.rs:557): uses row[displayField] as the label when it is a string, which a relation id is.
Suggested fix
-
In list.generated.tsx.jinja, give the primary branch the same relation cases as column: label from __display (falling back to the id), link to the row's own detail page.
-
Pick one for relation-valued @display:
- Resolve: when the target's display field is itself a to-one relation, run a second batched lookup against that relation's target, applying the same tenant and authorization filtering, and use its display value.
- Reject: refuse
@display on relation, array, composite, JSON and file fields at parse time, with a message suggesting a @computed text field.
Either way, display_value_to_string should not fall through to Display for Ref/RefArray.
-
Add a list-template test for a primary relation, and an API test for a display field that is a relation.
Summary
Relation labels (
<field>__display, #32) work for the common case, a to-one or to-many relation whose target's@displaynames a scalar field, shown in acolumncell or on the detail page. Two other shapes show ids instead of names:primarycolumn. The primary cell always renders the raw value throughformatFieldValue, so a relation there shows the target id even though the API returned<field>__display. A relation becomes primary through an explicit@list(primary), and also automatically when the schema's own@displaynames a relation field. That is the natural choice for a join entity.@displaynames a relation field. The server renders the display value withDynamicValue'sDisplayimpl, so__displayis the literal stringref(<id>). The generated detail and list pages then showref(product_01...). The edit-formRelationSelectfor such a target labels each option with the raw id. This affects every relation that points at such a schema, including derived inverse collections (-> X[]), which is where it typically shows up.Version checked
v0.45.0 release binary (x86_64 Linux, PostgreSQL build), source at tag
v0.45.0(10ec403), PostgreSQL 16.Reproduction
Create a Product "Blue widget", an Order "First order" and an OrderLine pointing at both, then read them (
GET .../entities/{id}and list give the same result):Generated site (
schemaforge site generate -s schemas -o app):/app/order-line:@display("product")promotesproductto the primary column. The generated cell isformatFieldValue(row.original.product, { kind: "relation_one" }), which rendersproduct_01..., althoughproduct__displayis"Blue widget"./app/order/<id>: the Lines row shows a link labelledref(product_01m3d41b6yevprsbadszjnera2).-> OrderLinepicker labels each optionproduct_01....Shapes that work:
order/productas@list(column)cells and on the OrderLine detail page, and stored or derived-> X[]where X's@displayis a scalar.Expected
A relation in the primary column shows its
__displaylabel, as acolumncell does, while still linking to the row's own detail page.For a target whose
@displayis a relation, either:lines__displayis["Blue widget"]; or@displayon a relation field (or on anything that is not a scalar) at parse time, so the problem cannot be declared.At the very least,
__displayshould never be the internalref(...)rendering.Source
crates/schema-forge-cli/templates/site/src/app/pages/list.generated.tsx.jinja:78-101: theprimarybranch handlesenumspecially and sends everything else, relations included, toformatFieldValue. Thecolumnbranch (:106) has, at:114-143, therelation_one/relation_many+relation_display_fieldcases that read__display.crates/schema-forge-cli/src/commands/site/context.rs:195-209:@display("<field>")auto-promotes that field toprimary, even when it is a relation.crates/schema-forge-acton/src/routes/entities.rs:1875-1876:display_value_to_string(display_value)on the target's display field.:2397-2405: non-scalar values fall back toother.to_string().crates/schema-forge-core/src/types/dynamic_value.rs:76:Self::Ref(id) => write!(f, "ref({id})").crates/schema-forge-core/src/types/schema_definition.rs:119-124and the@displayparser (crates/schema-forge-dsl/src/parser.rs:433): no check on the named field's type.labelForin the vendoredsrc/components/ui/relation-select.tsx(crates/schema-forge-cli/src/commands/site/vendor.rs:557): usesrow[displayField]as the label when it is a string, which a relation id is.Suggested fix
In
list.generated.tsx.jinja, give theprimarybranch the same relation cases ascolumn: label from__display(falling back to the id), link to the row's own detail page.Pick one for relation-valued
@display:@displayon relation, array, composite, JSON and file fields at parse time, with a message suggesting a@computed text field.Either way,
display_value_to_stringshould not fall through toDisplayforRef/RefArray.Add a list-template test for a primary relation, and an API test for a display field that is a relation.