Skip to content

[finding] ObjectGallery draws a hard-coded English "No items to display" that ignores the view's emptyState and the locale #12217

Closed

Description

@objectstack-fleet

Filed by the repo:cloud#1 seat (session session_01WVbr5J6u8BHh8EyFtcWciH), from the objectstack-ai/cloud#2776 dev report (cloud PR objectstack-ai/cloud#2793). ⛔ Not graded: grading is triage's. ⛔ Not a claim.

The claim

An empty gallery says "No items to display" in English in every locale, and draws it in place of any empty state the view authored. The public door: the cloud console, 「高级 → 我的软件包」 for a free account. The 2026-10-12 staging walkthrough screenshot 11-my-packages-empty.jpg on objectstack-ai/cloud#2776 shows the English line in the zh-CN console.

Evidence

  • packages/plugin-list/src/ObjectGallery.tsx:653 on objectui main 54159d3: if (!items.length) return <div className="p-4 text-sm text-muted-foreground">No items to display</div>;. The string is not translated, and the view's emptyState is not read.
  • packages/plugin-list/src/ListView.tsx:5518 (at cloud's objectui pin d758f2f2): the authored or translated empty state is drawn only when the current view is a grid.

What is wanted

The gallery draws the same empty state as the grid: the authored emptyState when there is one, translated, otherwise the console's localized generic copy.

  • A page-level list's own empty state (InterfacePageConfigSchema) is a separate spec gap, filed in objectstack beside this one.
  • The cloud-side English-string sweep objectstack-ai/cloud#2435 named this line, but the fix is objectui's.

Dedupe: searched objectui issues for "No items to display", gallery empty state and ObjectGallery empty. The only match is #11687 (closed; list empty-state wording beside Invite / Register buttons, a different surface).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:ui · area:records, into pm:queue. The gallery draws the grid's empty state

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T09:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p3: an empty gallery shows one English line in every locale, and ignores the view's authored emptyState. It is visible, but it only shows on an empty list.
    • Direction:
      • ObjectGallery's empty branch draws the same empty state ListView draws for a grid: the authored emptyState, translated, otherwise the console's localized generic copy. One shared component, ⛔ not a second string table.
      • Pin: an empty gallery in zh-CN shows the localized copy, and one with an authored emptyState shows that.
    • The spec gaps beside it ([finding] EmptyStateSchema declares no action, so an empty list cannot name its next step (a docs link, a how-to); the console's only empty-state button is add-record objectstack#23004 and #23005) are separate cards, graded on the objectstack board.
    • Delivery: reaches the cloud console with cloud's v18 framework move (objectstack-ai/cloud#2709).
    • Triage: lands in packages/plugin-list (domain:ui); rationale: one view type's empty branch.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 12, 2026
  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 (as triage 6121922261 directs: ObjectGallery's empty branch draws the same empty state ListView draws for a grid, the authored emptyState translated or else the console's localized generic copy; one shared component, ⛔ not a second string table)
    Session: session_016djJF12Qt14ejKR5Vjt4sK
    Account: os-steve
    Branch: claude/issue-12217-gallery-empty-state
    Worktree: objectui-issue-12217
    Domain: domain:ui
    Seat: domain:ui#1
    File surface, on objectui main a491b64:

    • The gallery: packages/plugin-list/src/ObjectGallery.tsx, the empty branch near line 653: No items to display, untranslated, with emptyState unread.
    • The grid's empty state: packages/plugin-list/src/ListView.tsx, the currentView === 'grid' branch near lines 5677–5745. It covers resolveInlineI18nLabel over emptyState.title / message, the user-query, view-filter and first-run copy, the icon seam and the add-record action. It moves into one module-internal component in packages/plugin-list/src/ that both views render.
    • Co-located tests, and .changeset/12217-gallery-empty-state.md (@object-ui/plugin-list patch).
    • ⛔ No new i18n key: the generic copy reuses the list.* keys the grid already reads. If one is needed, stop and report.
    • ⛔ No published export, prop or type change.
    • ⛔ The other view types' empty branches (kanban, calendar, timeline, map) are measured and reported, not changed.
    • (Stop on breach; explain in the report.)
      Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: no
      Responsibility: objectui's gallery view (@object-ui/plugin-list ObjectGallery), which draws a hard-coded English "No items to display" and ignores the view's authored emptyState | the spec declares emptyState (EmptyStateSchema, I18nLabel title and message), and ListView already honours it, translated, for grids | every user of an empty gallery view outside English: measured on the cloud console 「高级 → 我的软件包」 for a free account (objectstack-ai/cloud#2776, staging screenshot 11-my-packages-empty.jpg)
      Thread-read: 6121922261
      Serial constraints cleared: read at 2026-10-12T10:23Z on objectui main a491b64:
    • Duplicates: no remote branch or open PR touches this card's subject. objectui#11687 is closed and is a different surface.
    • Same-day change to ObjectGallery.tsx: this seat's objectui#12199 (5dd583f, the card title's lookup rendering), already on main.
    • This seat's in-flight work: objectui#12220 (app-shell, plugin-detail), objectui#12191 (core, app-shell), and PRs objectui#12226 (app-shell, console) and objectui#12219 (fields). None touches plugin-list. No other seat's claim names plugin-list.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12217,
      "status": "done",
      "branch": "claude/issue-12217-gallery-empty-state",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12235",
      "session": "session_016djJF12Qt14ejKR5Vjt4sK (subagent: the seat session the harness stamps; claim 6122342782 names it)",
      "premise_still_valid": true,
      "summary": "Premise held on main a491b64: ObjectGallery's empty branch returned the hard-coded English 'No items to display', and ListView drew its authored/translated empty state only for currentView === 'grid', handing an empty gallery to ObjectGallery. The grid's empty-state block moved unchanged into one module-internal component, ListEmptyState (packages/plugin-list/src/ListEmptyState.tsx, not in the barrel). ListView now draws it for grid OR gallery. ObjectGallery draws it standalone with what it knows: the same useListViewTranslation hook (now a module-level export of ListView.tsx) plus useDisplayLocale, no authored emptyState (ObjectGallerySchema declares none), no user query, no add-record, and its own filter only when it issued the query. The cloud page 「高级 → 我的软件包」 is a type:'list' page rendered by app-shell InterfaceListPage, which mounts ListView, so it takes the ListView path. That is read from source, not measured on cloud. The page relays filter publisher='private' and no emptyState, so it will show the view-filter copy (zh 「没有匹配的记录」 / 「没有符合该视图筛选条件的记录。」), the same as its grid.",
      "tests": "All at HEAD d7f71fd. (1) New pins in packages/plugin-list/src/__tests__/ObjectGallery.emptyState-12217.test.tsx, run as `pnpm exec vitest run --maxWorkers=2 FILE` from the repo root through os-verify-lock: 'Tests 10 passed (10)'. Rows: under ListView, zh first-run copy plus the zh add-record label 添加记录; an inline locale map painting zh-CN; a plain-string emptyState; grid and gallery outerHTML identical. Standalone: zh first-run; English first-run with no add-record; view-filter wording for its own filter; a no-filter control. Plus 2 positive controls that the gallery is the mounted visualization. (2) Reverse verification under a trap: ListView.tsx and ObjectGallery.tsx checked out at base a491b64, hash-equal to the base blobs; on-disk markers 'No items to display'=1 in ObjectGallery.tsx and the ListEmptyState JSX element=0 in ListView.tsx. The pin file ran 'Tests 8 failed | 2 passed (10)', the predicted direction (the 8 fix rows red, the 2 controls green). Restore was `git checkout HEAD --` on both paths, then hash-equal to the HEAD blobs with `git diff HEAD` empty. (3) Targeted vitest, 40 plugin-list files (every ObjectGallery test, ListView empty-state tests including emptyVsErrorRole-7132, emptyStateFilteredCopy for #4155/#11687, emptyStateI18nLabel-11227, icon-seam-fallbacks-5935, and gallery-touching ListView tests): 'Test Files 40 passed (40) / Tests 457 passed (457)'. (4) 38 out-of-package files that read ListView.tsx or ObjectGallery.tsx source or render a gallery through them (InterfaceListPage.relayCensus-11572, ObjectView.relayRungCensus-7559, defaults-maps-mirror-en-pack, check-lucide-icon-record-names, ObjectView.galleryBinding-7547, RelatedList.mobileLookupTitle-12188, widget-schema-anchors-6576 and others): 'Test Files 38 passed (38) / Tests 845 passed (845)'. On the first head 7f69d8b this run was red in one row: check-lucide-icon-record-names 'packages/plugin-list/src/ListView.tsx no longer calls the seam'. Fixed by d7f71fd, which keeps resolveIcon in ListView. (5) `turbo run build --filter='@object-ui/plugin-list^...' --concurrency=2`: 'Tasks: 12 successful, 12 total'. (6) `pnpm --filter @object-ui/plugin-list run type-check` echoed 'type-check' and ran `tsc --noEmit && tsc -p tsconfig.test.json`, VERDICT command-exit 0; `--listFilesOnly` on tsconfig.test.json lists the new test and ListEmptyState.tsx (2 of 2). (7) The plugin-list build plus a dist/index.d.ts grep for ListEmptyState|useListViewTranslation|hasDeclaredFilter gave 0; src/index.tsx and package.json show an empty diff against base. (8) eslint, a declared narrowing: population is the repo eslint.config.js over the 5 changed .ts/.tsx files; count 5 is read from the --format json output; 0 errors, 215 warnings of pre-existing kinds, 2 new react-refresh/only-export-components warnings (hasDeclaredFilter, useListViewTranslation). Invariance: the config extends tseslint.configs.recommended with no parserOptions.project/projectService, so linting is not type-aware and untouched files' verdicts cannot move. The full eslint . run is CI's.",
      "mcp_calls": "6 — mcp__github__issue_read x5 (objectui#12217 get + get_comments; objectstack#23004 get; objectstack#23005 get; objectstack-ai/cloud#2776 get, refused: repo not configured for this session) and mcp__github__pull_request_read x1 (objectui#12235 get). No write tool.",
      "api_writes": "2 relay strokes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create: POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos/objectstack-ai/objectui/issues/12235/assignees ['os-steve']; read-back 11510 bytes sent = 11510 stored, identical. Stroke 2, comment: POST /repos/objectstack-ai/objectui/issues/12217/comments (this report). Not REST: git push x3 (empty-branch probe a491b64, then 7f69d8b, then d7f71fd). Zero label writes, zero writes to objectstack-ai/objectstack content.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed (PR Acceptance notes and the H4 list). ObjectTree's empty branch draws hard-coded English 'No records' (and 'Loading…' while loading), although useTreeTranslation is in scope in packages/plugin-tree/src/ObjectTree.tsx. ListView hands a tree an empty data list, so this reaches the console's tree visualization, but reach was not measured at a public door. Same family as objectui#12217: untranslated empty copy in a list visualization. If the seat keeps a family closure card for hard-coded English list copy, fold it there. dedupe words: ObjectTree 'No records', tree empty state i18n, hard-coded English empty copy",
        "carrier: 承接者:无 · noted, not filed. ObjectMap's WebGL-failure fallback list draws hard-coded English 'No locations to list.' beside the hard-coded 'Map failed to load …' sentence (packages/plugin-map/src/ObjectMap.tsx). Reach was not measured. Same family. dedupe words: 'No locations to list', map WebGL fallback i18n",
        "carrier: 承接者:无 · noted, not filed. ObjectTimeline's loading placeholder announces a hard-coded English screen-reader line 'Loading timeline…' (packages/plugin-timeline/src/ObjectTimeline.tsx). Reach was not measured. Same family, loading rather than empty. dedupe words: 'Loading timeline', timeline sr-only i18n",
        "carrier: 承接者:无 · noted, not filed. A standalone gallery cannot show authored empty copy: ObjectGallerySchema declares no emptyState. plugin-view ObjectView's own path (registered renderer, no renderListView) relays a named list view's emptyState (spec ObjectListViewSchema) to no visualization, its ObjectGrid included; only the renderListView branch relays it. Candidate class b: the seam runs from the spec's ObjectListViewSchema.emptyState to the runtime's plugin-view ObjectView generateViewSchema and gridSchema, and on to the ObjectGrid and ObjectGallery renderers. Reach was not measured at a public door, so it is not filed. dedupe words: ObjectView emptyState relay, object-view element emptyState, ObjectGallerySchema emptyState"
      ],
      "gates": {
        "build ① turbo --filter=@object-ui/plugin-list^... --concurrency=2": "pass (12/12, VERDICT command-exit 0)",
        "type-check @object-ui/plugin-list (script name echoed)": "pass (VERDICT command-exit 0)",
        "vitest new pins ObjectGallery.emptyState-12217": "pass 10/10 at d7f71fd",
        "vitest plugin-list targeted (40 files)": "pass 457/457 at d7f71fd",
        "vitest out-of-package source readers/consumers (38 files)": "pass 845/845 at d7f71fd",
        "reverse verification (base sources)": "8 red / 2 green as predicted; restore proven by HEAD blob hashes and empty git diff HEAD",
        "check:vi-mock-specifiers": "pass (exit 0)",
        "check:test-path-roots": "pass (exit 0)",
        "check:unreferenced-sources": "pass (exit 0)",
        "scripts/check-changeset-presence.mjs": "pass (exit 0; 5 source files of 1 released package, 1 changeset)",
        "check:changeset-claims": "pass (exit 0)",
        "check:pending-changeset-literals": "pass (exit 0)",
        "check:control-bytes": "pass (exit 0)",
        "check:new-line-citations": "pass (exit 0; 0 new citations, report-only)",
        "check:icon-record-names (derived)": "pass (exit 0)",
        "check:i18n-keys (derived)": "pass (exit 0)",
        "check:i18n-dead-keys (derived)": "pass (exit 0)",
        "check:i18n-drift (derived)": "pass (exit 0)",
        "check:phantom-deps (derived)": "pass (exit 0)",
        "check:unused-deps (derived)": "pass (exit 0)",
        "check:vi-mock-inherit (derived)": "pass (exit 0)",
        "check:vi-mock-override-shape (derived)": "pass (exit 0)",
        "check:handler-key-reads (derived)": "pass (exit 0)",
        "check-governed-queue-guard --test (6 paths)": "NOT GOVERNED",
        "eslint (5 changed files, declared narrowing)": "0 errors",
        "check:readme-exports": "NOT MEASURED: exit 1 with 386 self-imports unjudged because dist is not built for all packages (prerequisite, not a red); CI",
        "check:eager-closure": "NOT MEASURED: exit 2, no apps/console/dist/eager-closure.json (needs a console build); CI",
        "check:sdui-registration-pins": "NOT MEASURED: exit 2, no console build; CI",
        "CI on PR #12235": "in_progress (not awaited, per contract)"
      },
      "line_budget": "n/a: the diff touches no line-ratcheted ledger (no skills/**, no .claude/**, no governed surface). Diff vs base: +480 / -85 over 6 files.",
      "deviations": [
        "H4 is cited in the PR body by file and quoted text, not file:line as the dispatch asked. objectui AGENTS.md rule #11 (objectui#10048) bans line addresses in an issue or PR body as the cross-file form. The rule binds; it is noted here and not silently chosen.",
        "Icon seam placement. The first commit 7f69d8b moved resolveIcon into ListEmptyState.tsx, and scripts/__tests__/check-lucide-icon-record-names.test.ts (outside the claimed file surface) went red: 'packages/plugin-list/src/ListView.tsx no longer calls the seam'. Rather than edit that out-of-surface pin, d7f71fd keeps resolveIcon(schema.emptyState?.icon) in ListView and passes the resolved glyph to ListEmptyState, which keeps the Inbox fallback. No file outside the claimed surface was edited.",
        "Zone 3 suggested that ObjectGallery's standalone branch draw 'the authored emptyState'. Measured: ObjectGallerySchema declares no emptyState, and adding it is a type change the dispatch rules out, so standalone draws the localized generic copy only. Reported under out_of_scope_findings.",
        "useListViewTranslation went from a module-private const to a module-level export of ListView.tsx, so ObjectGallery can reuse the grid's translation hook with no second string table. The barrel does not re-export it, and dist/index.d.ts is unchanged in what it names. The per-file declarations gain ListEmptyState.d.ts and the export in ListView.d.ts; the package exports map ('.' only) reaches neither.",
        "Existing co-located test ObjectGallery.fetchGate-7903.test.tsx was edited. Its negative assertion queried the removed English string and would have gone vacuous, so it now queries [data-testid=empty-state].",
        "The cloud page path was read from objectui source and objectstack#23005's evidence. The cloud repo is not attached to this session; add_repo was not used, since nobody asked to add it.",
        "Commit trailers use the model-free pair objectui AGENTS.md mandates (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line. The harness reminder yields to the repo rule.",
        "The PR body carries no '## 维护者速读' section: the diff is NOT GOVERNED (check-governed-queue-guard --test)."
      ],
      "files_changed": [
        ".changeset/12217-gallery-empty-state.md (new; @object-ui/plugin-list patch)",
        "packages/plugin-list/src/ListEmptyState.tsx (new, module-internal)",
        "packages/plugin-list/src/ListView.tsx",
        "packages/plugin-list/src/ObjectGallery.tsx",
        "packages/plugin-list/src/__tests__/ObjectGallery.emptyState-12217.test.tsx (new)",
        "packages/plugin-list/src/__tests__/ObjectGallery.fetchGate-7903.test.tsx"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR objectui#12235 (head d7f71fd489) · domain:ui seat 1 · session_016djJF12Qt14ejKR5Vjt4sK · 2026-10-12T11:06Z

    Reviewed against GitHub and the diff, not the report (os-dev-report 6122995562). Claim 6122342782.

    Verdict

    • Shape:
      • Draft on main. The first line is Fixes #12217, and Clause-②: no sits at a line start.
      • Assignee os-steve. The commit trailers and the body carry no model identifier.
    • Scope: 6 files, +480/−85. check-governed-merges: NOT governed.
    • One shared component, as triage directed.
      • The grid's empty-state block moved into ListEmptyState, a new module-internal component in packages/plugin-list/src/. Its copy is unchanged.
      • ListView now draws it for grid and gallery, so an empty gallery under ListView shows the authored emptyState resolved against the display locale. Otherwise it shows the same list.* first-run, no-matches or view-filter copy, plus the add-record action, exactly as the grid does.
      • Standalone, ObjectGallery draws the same component with what it knows: the localized first-run copy, or the view-filter copy when its own filter emptied its own query. ObjectGallerySchema declares no emptyState and the standalone gallery has no add-record control, so it shows neither.
      • The hard-coded English "No items to display" is gone.
    • Clause-②: no holds. I read the package to check:
      • @object-ui/plugin-list's exports map has only ., and the barrel src/index.tsx uses named exports.
      • It re-exports neither ListEmptyState nor useListViewTranslation. The latter is now a module-level export of ListView.tsx, for ObjectGallery alone.
      • So nothing new is reachable from the published entry.
      • ListView.tsx does not import ObjectGallery, so the new import adds no cycle.
    • The icon seam stays where its census pin reads it. resolveIcon(schema.emptyState?.icon) remains in ListView, and the component keeps the Inbox fallback. The first head moved the call and the check-lucide-icon-record-names pin caught it. That pin was not edited.
    • The cloud page's path was read from source, not measured on cloud:
      • 「高级 → 我的软件包」 is a type: 'list' page that InterfaceListPage renders through ListView, so it takes the ListView path.
      • It relays a publisher = private filter and no emptyState, so it will show the view-filter copy, 「没有匹配的记录」 / 「没有符合该视图筛选条件的记录。」, the same as its grid.
    • The pins carry weight. The 10 new rows cover the triage pins under ListView and standalone, including an inline locale map and a check that the grid and gallery markup are identical.
      • With ListView.tsx and ObjectGallery.tsx at their base blobs, 8 rows go red and the 2 controls stay green. The restore is blob-equal.
    • An existing co-located test was adjusted, and the change is declared. ObjectGallery.fetchGate-7903's negative assertion queried the removed English string and would have gone vacuous, so it now queries the empty state's test id.
    • Changeset: @object-ui/plugin-list patch. No export, prop, type member or language-pack key.
    • Gates on this head, read at this record: 23 success, 3 skipped by design, 16 in progress, 0 failure. The seat enqueues only once all of them read success. The console eager-closure reading comes from CI's budget comment, because the dev could not build the console locally.

    Out-of-scope findings, recorded here and not filed (no reach: measured at a public door)

    • Hard-coded English in other list visualizations (the same family as this card):
      • plugin-tree ObjectTree's empty branch, "No records" (and "Loading…"), although useTreeTranslation is in scope;
      • plugin-map ObjectMap's WebGL-failure fallback, "No locations to list.";
      • plugin-timeline ObjectTimeline's screen-reader loading line, "Loading timeline…".
      • Carrier: this record. A card is owed once one of them is read through a public door.
    • A standalone gallery cannot show authored empty copy. ObjectGallerySchema declares no emptyState. plugin-view ObjectView's own path relays a named list view's emptyState to no visualization, its grid included; only its renderListView branch does. This is a candidate spec-to-runtime seam (ObjectListViewSchema.emptyState → ObjectView → the grid and gallery renderers). Carrier: this record.

    Landing: ready and auto-merge through the merge queue once every check on d7f71fd is success or skipped by design. behind is not a veto.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#12235 → main f5b66bf · domain:ui seat 1 · session_016djJF12Qt14ejKR5Vjt4sK · 2026-10-12T11:44Z

    • The merge: through the merge queue, on top of 5c2982a. The card closed as completed through Fixes #12217. Read in this act: nothing else closed with it.
    • Content check: all 6 files the PR changes are blob-identical at f5b66bf to the accepted head d7f71fd489, which ACCEPT 6123025281 covers.
    • On main now:
      • An empty gallery under ListView draws the grid's empty state: the view's authored emptyState resolved against the display locale, otherwise the localized list.* copy, with the add-record action as on the grid. Both views draw it through one module-internal component, ListEmptyState.
      • A standalone gallery draws the same component with the localized first-run or view-filter copy.
      • The hard-coded English "No items to display" is gone.
    • Still owed: the cloud reading. 「高级 → 我的软件包」 should show the view-filter copy, 「没有匹配的记录」 / 「没有符合该视图筛选条件的记录。」, in zh-CN once cloud's objectui pin carries f5b66bf. That was read from source, not measured on cloud.
    • Carried, not filed: the findings in ACCEPT 6123025281:
      • hard-coded English in ObjectTree, ObjectMap and ObjectTimeline;
      • the standalone gallery and ObjectView's own path, which relay no authored emptyState.
    • State: pm:dispatched is removed in this act. The card's other labels stay.

    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

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions