Skip to content

intent: emptyText: on a field names what an empty value reads as - "No limit" or "Never" instead of a hidden row or an em dash (#8605) - #8606

Merged
iliyan-velichkov merged 2 commits into
masterfrom
issue-8605-field-empty-text
Nov 13, 2026
Merged

iliyan-velichkov merged 2 commits into
masterfrom
issue-8605-field-empty-text

Conversation

@iliyan-velichkov

@iliyan-velichkov iliyan-velichkov commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Cause

A generated page had two ways to show a field whose value is empty, and neither could say what the empty value means. The Details rail hid the row (and the whole Details block when every value in it was empty, #7522); every other surface printed "—". A reader could not tell "no limit" from "not loaded" or "not filled in yet", and the only workaround was a sentinel value (-1, 9999-12-31) that every rule, check and report then had to know about.

Change

A new field key, as the issue designs it:

- { name: maxLoans,  type: integer, readOnly: true, label: Loan limit, emptyText: No limit }
- { name: expiresOn, type: date, label: Membership expires, emptyText: Never }
- { name: nickname,  type: string, length: 40, emptyText: Not set }
  • Model / parser - FieldIntent.emptyText; IntentParser.validateEmptyText refuses it blank, on a boolean, a uuid, the primary key, a required field, a number:, placement: hidden, an aggregate, the document title and a chat messageBody, each naming the entity and field. On a relation it stays an unknown key (pinned by a test).
  • Generator - EdmIntentGenerator emits the trimmed scalar widgetEmptyText (both twins) and carries it on a related: register column; ModelParameterProcessor escapes widgetEmptyTextJs (templates: an authored field label: is written raw into the Harmonia report JS and the form T() fallback - an apostrophe blanks the page (#7207 class) #7294) and copies emptyText/emptyTextJs onto a register column; ModelTranslations seeds t.<DATA_NAME>_empty beside the label.
  • Runtime - one helper, basePage.orEmptyText(value, shown, tkey, text) (empty = null / undefined / '' / []); detailPanel.cellValue wraps its result with it, so composition panels and related: registers follow (a cross-project register has no emptyTkey and prints the authored text).
  • Templates - the six view templates' #valueDisplay (manage form, power/my/partner document, my/partner form) print the text; #valueGate keeps only the canSee / visibleWhen terms, so the rail row always shows, and hasDetailValues counts the field whenever the caller may see it; the text/email/url/tel/number/textarea inputs get it as :placeholder (date-family pickers unchanged); the subtitle (subtitleParts) and the document summary line; the manage and plain list cells and preview sheets; the my/partner lists' display(); the detail and related register columns. exportCell is unchanged, so exports keep ''.
  • Byte-identical when absent - every template change is guarded by widgetEmptyText, and widgetEmptyText / widgetEmptyTextJs / the _empty catalog entry are emitted only when authored.
  • Docs - assistant guide, .claude/docs/intent-dsl-features.md, .claude/docs/harmonia-ui.md.

Verification

  • mvn -T 1C formatter:validate after wiping the formatter cache - BUILD SUCCESS.
  • Unit suites: engine-intent 2121 and ide-template 359 green, including the new FieldEmptyTextIntentTest (12), EdmFieldEmptyTextTest (3) and ModelParameterProcessorTest#anEmptyTextIsEscapedForTheTemplates / #anEmptyTextSeedsItsCatalogEntryBesideTheLabel / #aRelatedColumnCarriesTheEmptyText; ConsumedAttributesAuditTest green.
  • Integration (local, headless): IntentEmissionCoverageIT (new TeamNote.reviewBy: the record page wraps it, the rail has no emptiness gate for it, other surfaces name the key, the en-US catalog holds TEAM_NOTE_REVIEW_BY_empty: Not scheduled, no page of a field without the key carries any emptyText code, and a created record reads it back null), the new FieldEmptyTextHarmoniaIT (record page rail "Loan limit / No limit" with the Details block shown, "Never" / "Not set" in the main column, "Not returned" in the composition panel, the list cells, the edit form's placeholder, a filled member printing its values with no emptyText, REST answering null), FieldPlacementHarmoniaIT, RecordHeaderHarmoniaIT - all green.
  • HarmoniaFieldDisplayIT (smoke gate) matched the value macros by their exact text and failed on the first CI run; it now asserts the new shape (the #{else} branch is the unchanged one, both gate branches ask canSee()) and runs basePage.orEmptyText in GraalJS - 9/9 locally.
  • Byte-identical: the 14 changed templates rendered from origin/master and from this branch with a context carrying no widgetEmptyText (fresh Velocity engine per template) produce identical output.

Not verified / not covered:

  • The CSV export is not driven in the browser; exportCell is unchanged code.
  • The my/partner pages and the list preview sheets are covered by template rendering only, not driven in a browser.
  • The release-profile javadoc build was not run; the only Javadoc added is on FieldIntent, which is not a published SDK type.
  • Not in the issue's surface list, so left out: the document's own items table (cellDisplay) and the my/partner child tables (displayChild). Both still print "—"; changing them would also touch every document model's output.

Documentation

Fixes #8605

🤖 Generated with Claude Code

…o limit" or "Never" instead of a hidden row or an em dash (#8605)

Cause: a generated page had two ways to show an empty field value and neither
could say what the emptiness means. The Details rail hid the row (and the whole
block when every value was empty, #7522); every other surface printed an em
dash. A reader could not tell "no limit" from "not loaded", and the only
workaround was a sentinel value every rule, check and report had to know.

Change: a new field key `emptyText: <text>`.
- FieldIntent.emptyText; IntentParser.validateEmptyText refuses it blank, on a
  boolean, a uuid, the primary key, a required field, a number:, placement:
  hidden, an aggregate, the document title and a chat messageBody. On a
  relation it stays an unknown key.
- EdmIntentGenerator emits the trimmed scalar widgetEmptyText (also on a
  related: register column); ModelParameterProcessor escapes widgetEmptyTextJs
  and copies emptyText/emptyTextJs onto a register column; ModelTranslations
  seeds t.<DATA_NAME>_empty beside the label.
- Runtime: one helper, basePage.orEmptyText(value, shown, tkey, text);
  detailPanel.cellValue wraps its result with it (composition panels and
  related: registers).
- Templates: the six view templates' valueDisplay (manage form, power/my/partner
  document, my/partner form) print the text, valueGate keeps only the role and
  visibleWhen terms so the rail row always shows, hasDetailValues counts the
  field; text/email/url/tel/number/textarea inputs get it as placeholder; the
  subtitle and the document summary line, the manage and plain list cells and
  preview sheets, the my/partner lists' display() and the detail/related
  register columns carry it. Every change is guarded by widgetEmptyText, so a
  model without the key generates byte-identical output. Exports keep ''.
- Docs: assistant guide, intent-dsl-features.md, harmonia-ui.md.

Verified: formatter:validate (cache wiped); engine-intent 2121 and ide-template
359 unit tests green, incl. the new FieldEmptyTextIntentTest (12),
EdmFieldEmptyTextTest (3) and three ModelParameterProcessorTest cases;
IntentEmissionCoverageIT (TeamNote.reviewBy: emission, catalog, no emptyText
code on any other page, value stays null), FieldEmptyTextHarmoniaIT (new,
browser), FieldPlacementHarmoniaIT and RecordHeaderHarmoniaIT green. The 14
changed templates rendered before/after without the key are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue macros (#8605)

Cause: the structural guard matched the six view templates' #valueDisplay and
#valueGate macros by their exact text; #8605 wraps both in an
#if($property.widgetEmptyText) branch, so the smoke gate's
everyReadOnlyCardPrintsThroughTheSharedHelper failed although every value still
prints through fieldDisplay()/fieldText() and a role-scoped field still asks
canSee().

Change: the guard asserts the new macro shape - the #{else} branch is the
unchanged one, the emptyText branch wraps the same formatted value in
orEmptyText - and that both branches of the gate ask canSee(). A new GraalJS
test runs basePage.orEmptyText: null / undefined / '' / [] read as the text,
the text resolves through its catalog key, a value (zero included) passes
through.

Verified: HarmoniaFieldDisplayIT 9/9 locally; formatter:validate (cache wiped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iliyan-velichkov
iliyan-velichkov merged commit eff9a83 into master Nov 13, 2026
16 checks passed
@iliyan-velichkov
iliyan-velichkov deleted the issue-8605-field-empty-text branch November 13, 2026 14:20
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: emptyText: on a field names what an empty value reads as - "No limit" or "Never" instead of a hidden row or an em dash

1 participant