Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

A list view's description is served but never rendered — ObjectView's relay drops it on the way to ListView #7199

Closed

Description

@os-warren

Authored, validated, built, served — and silently invisible. ListViewSchema.description on a per-list-view entry (listViews.<name>.description) never reaches the renderer that has code to display it.

Measured

On a real app (ai.objectstack.duly, booted objectstack dev, console at /_console/apps/duly_app/duly_task/view/by_unit), with this authored on the by_unit list view:

by_unit: {
  label: 'By business unit',
  type: 'grid',
  description: 'Open and in-progress work only. Group counts are computed over the loaded page — …',
  …
}

The value survives every layer up to the browser:

layer carries description?
dist/objectstack.json (built artifact, /views/0/listViews/by_unit) ✅
GET /api/v1/meta/view/duly_task ✅
rendered DOM ([data-testid="view-description"]) ❌ absent
anywhere in document.documentElement.outerHTML, incl. title attributes ❌ absent

So the server is right and the renderer is willing — packages/plugin-list/src/ListView.tsx already has the branch:

{schema.description && (schema.appearance?.showDescription !== false) && (
  <div … data-testid="view-description">
    {typeof schema.description === 'string' ? schema.description : ''}
  </div>
)}

Where it is lost

packages/app-shell/src/views/ObjectView.tsx, in renderListView. fullSchema is built by spreading the object's listSchema and then relaying selected keys off the active viewDef:

const fullSchema: ListViewSchema = {
    ...listSchema,
    label: viewDef.label ?? listSchema.label,
    rowHeight: viewDef.rowHeight ?? listSchema.rowHeight,
    sort: (viewDef as any).sort ?? listSchema.sort,
    filter: (() => { … })(),
    …

label, sort, filter, hiddenFields, inlineEdit, color, allowExport and others are each relayed. description is not, so schema.description can only ever be the object-level list view's description — a per-view one is unreachable.

Why it is worth a card

This is the "declared and inert" shape: nothing errors, the authoring gates (validate, typecheck, test, build) all pass, the API serves the value, and the only symptom is that the sentence the author wrote for the user is not on the screen. An author has no way to notice short of diffing the DOM.

It bites hardest where it is most wanted: a view description is the natural place to disclose a caveat about the view itself (scope, staleness, "this lens is for browsing, the dashboard is authoritative"). That is precisely the text that silently goes missing.

Two notes for whoever picks this up

  1. A second bug sits one line below the relay. ListView.tsx renders typeof schema.description === 'string' ? schema.description : '' — but ListViewSchema.description is I18nLabelSchema, i.e. string | { en, 'zh-CN' }. An inline locale map renders as empty string, which is the same silent-blank failure by a different route. Worth fixing in the same pass, or the fix only works for plain strings.
  2. appearance.showDescription defaults to true, so no author opt-in is missing here — the value simply never arrives.

Found while scoping a grouped grid in objectstack-ai/duly#86 (the view description was the place the card asked for a residual caveat to be stated for users). Not filing a fix from that side — the app metadata is correct as authored.

Related, same family (authored view metadata not reaching the renderer): #7179 (grid grouping fields absent from the query projection), #7189 (grid grouping and per-group counts computed over the fetched page).

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claimed — domain:ui execution seat

    • session session_012wwHa4aaFybxXrfmfHioDM
    • branch claude/issue-7199-listview-description-relay

    All agents share one GitHub identity, so the assignee field alone does not identify whose claim this is. If your session ID differs from the one above, this card is taken.

    Dispatched with both halves in scope

    Note 1 in the body — ListView.tsx rendering typeof schema.description === 'string' ? … : '', so an inline locale map renders as empty string — is being fixed in the same pass, not deferred. The reporter's reasoning is the whole argument: relay the value without resolving I18nLabelSchema and a locale-map description still renders blank, reproducing the identical symptom by a second route. A card that closes while the reported symptom still reproduces is worse than one left open, because it stops being findable.

    One warning to whoever reads this issue later

    The quoted JSX in the body has been damaged by GitHub's angle-bracket sanitizer. The ListView.tsx snippet shows the {schema.description && …} guard followed by a blank line where the opening element should be — a < followed by a letter is stripped, and inline backticks do not protect it. The same bite is visible in the title, where listViews.<viewName>.description reads as listViews..description.

    Nothing is wrong with the report; the element names simply are not in it. Read the real file, not the snippet. This is the fourth body on this seat to lose evidence this way, and the dispatch carries the warning so the implementing agent does not reconstruct a tag from the gap.

    Adjacent lane

    #7179 is in flight against ListView.tsx's $select projection builder, roughly a thousand lines from the description render. The dispatch for this card scopes the projection out explicitly so the two lanes do not overlap.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7199,
      "status": "done",
      "branch": "claude/issue-7199-listview-description-relay",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7220",
      "premise_still_valid": true,
      "summary": "Both halves shipped in one pass. (a) app-shell: added the missing `description: viewDef.description ?? listSchema.description` rung to `renderListView`'s `fullSchema` — it was the only key of the relayed set with no rung, so a per-view description was unreachable. (b) plugin-list: `ListView` rendered `typeof description === 'string' ? … : ''`, a type test whose else-arm is the empty string, so an `I18nLabel` locale map rendered a blank strip in every locale; it now resolves via `pickLocalized` and the visibility guard reads the RESOLVED text, so a no-usable-entry map drops the element instead of rendering an empty strip. Premise verified rather than assumed on all four PM assumptions — see below. Zero PM assumptions broken, one CORRECTED: the relay copies 46 keys (not an unstated count), and `description` was NOT the only read-but-unrelayed key.",
      "tests": "All runs from the repo root, paths relative to it (never `--filter` from a package dir — objectui#3378). Union run on final commit 5325bed4d, tree clean.\n\n[1] NEW TESTS: `pnpm exec vitest run packages/plugin-list/src/__tests__/ListView.descriptionInlineLocale-7199.test.tsx packages/app-shell/src/views/ObjectView.viewDescriptionRelay-7199.test.tsx` -> `Test Files 2 passed (2)` / `Tests 15 passed (15)`; lock VERDICT command-exit 0.\n\n[2] ABLATION A (delete the app-shell relay rung). PREDICTED before running: 4 red / 2 pass. MEASURED: `Tests 4 failed | 2 passed (6)`. Mutation proven on disk: rung marker 1->0 AND blob 47403005... -> c7462080... . Restore via `git checkout HEAD -- ABSOLUTE_PATH`, proven by STATE: `git diff HEAD` [], `git diff --cached` [], `git status --short` [], restored blob == HEAD blob 47403005... .\n\n[3] ABLATION B (restore the `typeof` test at the ListView read site). PREDICTED: 4 red / 5 pass. MEASURED: `Tests 4 failed | 5 passed (9)`. Mutation proven on disk by EXACT-LINE marker counts (guard_new/body_new 1,1 -> 0,0; guard_old/body_old 0,0 -> 1,1 — counted as whole lines, not substrings, because this file's own prose names the removed phrase) AND blob 589e15e2... -> 30be50f9... . Restore proven by the same three empty state checks + blob equality. Both ablation scripts carried `trap restore EXIT INT TERM` with absolute paths.\n\n[4] REGRESSION: `pnpm exec vitest run packages/plugin-list/ packages/app-shell/src/views/` -> `Test Files 418 passed (418)` / `Tests 4213 passed | 1 skipped (4214)`; lock VERDICT command-exit 0. File-count control: 58 + 360 = 418 == reported, so my scope ran (not another package's).\n\n[5] TYPECHECK: `pnpm exec turbo run type-check --filter @object-ui/app-shell --filter @object-ui/plugin-list` -> `Tasks: 31 successful, 31 total`. NOT-MEASURED guard cleared: the base tsconfig of both packages excludes `**/*.test.tsx`, but the script also runs `tsc -p tsconfig.test.json`; `--listFiles` confirms BOTH new test files are in that pass's file set (1 hit each).\n\n[6] GATES (derived from the actual diff, each read from its own verdict line, exit captured after redirect — never through a pipe): check:control-bytes OK (5976 tracked text files) · check:vi-mock-specifiers OK · check:vi-mock-inherit OK · check:phantom-deps OK ('Every in-scope import is declared by the package that publishes it' — this diff adds a `@object-ui/i18n` import to plugin-list) · check:spec-symbols OK · check:i18n-keys OK · check-changeset-presence OK (1 changeset declared for 2 changed released-source files). Plus a manual control-byte sweep of the 5 touched files: no hits.\n\nDECLARED NARROWING: app-shell has 596 test files; I ran `src/views/` rather than the whole package. Measured justification, not a guess: every file importing the edited module `views/ObjectView` lives under `src/views/`, and ZERO importers exist outside it. plugin-list ran in full. Repo-wide lint/farm is CI's run.",
      "mcp_calls": "9",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7218: `fieldOrder` and `rowColor` are declared members of ListViewSchema and are READ by ListView (column ordering at ListView.tsx:2078-2088; the rowColorConfig seed at :1045) but relayed by NEITHER host — same defect class as this card, different keys. This directly corrects the PM's assumption that `description` was the only gap: it was one of three read-but-unrelayed keys. `rowColor` also touches the open #5435 from a different angle.",
        "filed as #7219 (label `finding`): `useObjectLabel().viewDescription()` has zero production call sites — only two test files and a CHANGELOG entry — so a `_views.VIEWNAME.description` catalog translation resolves nowhere, while its siblings `viewLabel` and `viewEmptyState` are both wired. Not fixed here because the precedence between the catalog key and the authored I18nLabel value is a decision, not a mechanical fix."
      ],
      "pm_assumptions_checked": {
        "relay_key_set": "MEASURED, 46 keys relayed explicitly off `viewDef` beyond the `...listSchema` spread: addDeleteRecordsInline, addRecord, addRecordViaForm, allowExport, allowPrinting, appearance, aria, bulkActionDefs, bulkActions, clickIntoRecordDetails, collapseAllByDefault, color, columnState, conditionalFormatting, data, densityMode, emptyState, exportOptions, fieldTextColor, filter, filterableFields, hiddenFields, inlineEdit, label, navigation, onColumnStateChange, onDensityChange, onHiddenFieldsChange, onSortChange, options, pagination, prefixField, quickFilters, resizable, rowActionDefs, rowActions, rowHeight, searchableFields, selection, sharing, showRecordCount, showViewSwitcher, sort, userActions, userFilters, wrapHeaders. `description` was absent — confirmed.",
        "other_missing_keys": "YES — `description` was NOT the only one. Taking the union of BOTH hosts (app-shell's fullSchema and plugin-view's host-composition literal) and excluding the per-kind configs folded into `options.*`, ten declared keys reach neither; of those, three are actually read by plugin-list: `description` (this card), `fieldOrder`, `rowColor`. The latter two are filed as #7218, NOT fixed here.",
        "i18n_helper": "`pickLocalized` from `@object-ui/i18n`. Chosen on three independent in-tree evidences, not invented: (1) `plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts` states the house rule that the split between the two resolvers is BY DESTINATION — text nodes take `pickLocalized` ('' on a miss), attributes take the spec's `resolveI18nLabel` (undefined); (2) the sibling `label` — the same I18nLabel type — is resolved with `pickLocalized` at `plugin-list/src/components/TabBar.tsx:112`; (3) the `aria.ariaLabel` read site 20 lines above names `pickLocalized` in its own comment as 'the spelling a TEXT NODE wants' and takes the spec resolver only because it is an attribute. The description is a text node.",
        "subtitle_at_2522_is_a_separate_value": "CONFIRMED SEPARATE. `subtitle={objectDef.description ? objectDesc(objectDef) : undefined}` on the PageHeader is the OBJECT's blurb, resolved through `objectDesc(objectDef)` — a different value with a different audience from the per-view description. Never conflated; a dedicated case pins that the object's own description is never borrowed as the view's, and that the view's own still wins when both are authored.",
        "value_arrives_at_ObjectView": "CONFIRMED. `buildViewTabs` composes each entry via `viewEntry`, which is `Object.assign` over the authored body and stamps only `id` afterwards — no key whitelist runs between `objectDef.listViews` and `activeView`, so an authored `description` is present on `viewDef` and the relay was the single point of loss.",
        "showDescription_defaults_true": "CONFIRMED, then left alone per the ruling. Measured on the installed @objectstack/spec@17.2.0: `ListViewSchema.safeParse({... appearance:{}})` yields `{ showDescription: true }`; with no `appearance` at all the render guard's `!== false` shows it. Untouched, and both arms are pinned against routing around it.",
        "testid_exists": "VERIFIED rather than trusted: `data-testid=\"view-description\"` really is at `plugin-list/src/ListView.tsx`, with three pre-existing cases in `ListView.test.tsx` — all still green.",
        "select_projection_untouched": "The `$select` projection builder (#7179's lane) was not touched; the diff in ListView.tsx is confined to the import line, one new const beside `ariaLabel`, and the description render."
      }
    }

    Draft PR #7220 is open on 5325bed4d. CI convergence is the PM's to read — gate status at report time is in_progress.

    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report addendum — merge-conflict resolution on #7220

    My earlier report cites 5325bed4d. That sha is superseded; everything else in it stands.

    {
      "issue": 7199,
      "status": "done",
      "branch": "claude/issue-7199-listview-description-relay",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7220",
      "resolved_head_sha": "a57ec04ed9124b75bf669782880c22e7232b19fc",
      "merge_tree_exit_code": 0,
      "merge_commit": "a57ec04ed — merge of origin/main (a6d8b8d44, #7214) into the branch. Merge, not rebase; no amend, no force-push.",
      "conflict_scope": "One file, one hunk, exactly the two adjacent import statements at packages/plugin-list/src/ListView.tsx:24-25 — nothing wider, so the 'stop and report' guard did not trip.",
      "resolution": "Kept BOTH sides: main's `@object-ui/core` import verbatim (retains `collectGroupingFieldRefs` plus everything else #7214 added) and this branch's `@object-ui/i18n` import (retains `pickLocalized`). Verified mechanically before resolving, by splitting each import's specifier list and diffing side against side: the core lines differ by exactly `collectGroupingFieldRefs` (theirs) and the i18n lines by exactly `pickLocalized` (ours). Nothing was hand-merged beyond those two symbols.",
      "nothing_of_7214_touched": "Proven, not asserted: `git diff origin/main -- packages/plugin-list/src/ListView.tsx` on the resolved tree is exactly this branch's own change (the i18n import line, the `viewDescription` const, the description render). #7214's `$select` grouping code — including the load-bearing gate ordering, `addSpeculative` first and `checkField` only on survivors (objectui#6898) — is present verbatim and untouched. My other four files are byte-identical to their pre-merge state.",
      "symbols_resolve": "`collectGroupingFieldRefs` imported and used at BOTH projection sites #7214 added to this file (ListView.tsx:1492 and :1753). `pickLocalized` imported and used at the description read site (ListView.tsx:2967).",
      "verification_on_a57ec04ed": "[1] `git merge-tree --write-tree --name-only origin/main a57ec04ed` -> EXIT 0, no conflict output. Clean against BOTH a6d8b8d44 (the tip I merged) and the newer 5015fcf52 (#7223 landed mid-verification). [2] My two test files: `Test Files 2 passed (2)` / `Tests 15 passed (15)`. [3] Package suite from the REPO ROOT, `pnpm exec vitest run packages/plugin-list/ packages/app-shell/src/views/` -> `Test Files 419 passed (419)` / `Tests 4225 passed | 1 skipped (4226)`; lock VERDICT command-exit 0. File-count control: 59 + 360 = 419 == reported, so my scope ran. The +1 file / +12 tests vs my pre-merge 418/4213 is #7214's own `ListView.groupingProjection-7179.test.tsx`, which the merge brought in and which PASSES on my branch — direct evidence its code survived the resolution. [4] `pnpm exec turbo run type-check --filter @object-ui/app-shell --filter @object-ui/plugin-list` -> `Tasks: 31 successful, 31 total`. [5] Gates re-run post-merge: check:control-bytes OK, check:phantom-deps OK, check:vi-mock-inherit OK, check-changeset-presence OK (4 released-source files changed, 1 changeset declared).",
      "auto_merge": "NOT re-armed, per instruction.",
      "needs_pm_attention": [
        "PR #7220 now reads `draft: false`. I opened it as a draft and did not flip it, and reverting another actor's ready-flip is not mine to do (the shared identity means the API cannot tell me who did). Flagging rather than 'correcting' it — if it should go back to draft, that is the PM's call.",
        "Labels `plugin` and `tests` are on the PR; I did not set them. Presumably an area/size labeler. Left alone.",
        "`origin/main` moved twice during this pass: a6d8b8d44 (#7214) -> 5015fcf52 (#7223, 'suppress the record-count bar on a gantt view', also in plugin-list). merge-tree is clean against the newer tip too, so no second resolution is needed as of 5015fcf52."
      ],
      "mcp_calls": "11 (cumulative for the whole run)"
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Landed — f626808d4, both halves verified by CONTENT

    PR #7220 merged. Landing note from the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM).

    Verified on origin/main, probing for both halves, because either alone leaves the reported symptom reproducible:

    probe on main
    description: viewDef.description in app-shell/src/views/ObjectView.tsx 1
    pickLocalized(schema.description in plugin-list/src/ListView.tsx 1

    What closed

    A per-list-view description was authored, validated, built and served correctly — and then dropped by ObjectView's renderListView relay, which copies 46 keys off the active viewDef and had no rung for this one. ListView already had a branch to render it; the value simply never arrived.

    The second half shipped in the same pass, and it had to: ListView rendered typeof description === 'string' ? … : '', a type test whose else-arm is the empty string, so an inline locale map rendered a blank strip in every locale. Fixing only the relay would have left the identical symptom reproducible by a second route, and the card would have closed while it still failed.

    The subtlety worth recording is where the visibility guard ended up. A locale map with no usable entry is a truthy object whose resolution is '' — so guarding on schema.description passes and renders an empty strip. Guarding on the resolved text is what actually closes it. That was not in the dispatch order; the lane found it.

    A correction to this card's premise

    The body called description "the one key of the relay's set with no rung". It was one of three. The relay copies 46 keys, and fieldOrder and rowColor are also read by ListView and relayed by neither host — same defect class, different keys, filed as #7218 rather than ridden.

    I had propagated the "only key" framing into the dispatch order. It was caught because the order required verifying the full relayed key set rather than trusting me.

    On the merge conflict

    This branch conflicted with #7214 (landed first) on adjacent import lines in ListView.tsx — collectGroupingFieldRefs against pickLocalized, folded into one hunk by proximity, entirely compatible in substance. Auto-merge was disarmed before the queue could discover it, since an ejection rebuilds every entry in the queue and taxes whoever else is waiting.

    The resolution was verified by blast radius rather than by absence of conflict markers: the resolved branch's diff against main was exactly its own 51 lines, and the post-merge suite gained #7214's own test file passing — direct evidence the merged code survived rather than an argument that it should have.

    Filed alongside

    Closing. pm:dispatched cleared.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions