Skip to content

fix(plugin-list): an empty gallery draws the grid empty state, translated (objectui#12217) - #12235

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12217-gallery-empty-state
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12217-gallery-empty-state

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12217
Clause-②: no

An empty gallery now draws the same empty state an empty grid draws: the view's authored emptyState (title and message resolved against the display locale, its icon), otherwise the localized list.* copy. It used to draw a hard-coded English "No items to display" in every locale and ignored the authored emptyState.

What changed

  • One shared component, ListEmptyState (new, module-internal: packages/plugin-list/src/ListEmptyState.tsx, not in the package barrel). The grid's inline empty-state block in ListView moved into it unchanged: the copy selection between the user's query, the view filter and first run (objectui#4155, objectui#11687), the I18nLabel resolution of title and message (objectui#11227), the Inbox glyph fallback, and the add-record action. No second string table, no new key.
  • ListView draws it for gallery as it does for grid (the empty branch's condition is now grid or gallery). The authored icon name is still resolved through resolveIcon inside ListView, and the component takes the resolved glyph. So ListView.tsx still calls the icon seam, which scripts/__tests__/check-lucide-icon-record-names.test.ts pins by path, and the change stays inside the claimed file surface.
  • ObjectGallery's empty branch draws it standalone, through the translation hook ListView draws it with (useListViewTranslation, now a module-level export of ListView.tsx; the barrel does not re-export it) and useDisplayLocale().

H1: the two paths a gallery reaches, and the cloud page

Path Before After
Under ListView with viewType: 'gallery': the console's object list (app-shell ObjectView hands plugin-view's ObjectView a renderListView that mounts ListView) and an interface list page whose visualization is a gallery ListView's empty branch was grid-only, so the empty list went through SchemaRenderer to ObjectGallery, which drew the English line ListView draws ListEmptyState itself; ObjectGallery is not mounted for an empty list
Standalone: an authored object-gallery node, plugin-view ObjectView's own non-grid path (no renderListView), RelatedList's mobile card list ObjectGallery drew the English line ObjectGallery draws ListEmptyState

The cloud page 「高级 → 我的软件包」 takes the ListView path. It is a type: 'list' page (cloud's sys-package.page.ts, as objectstack-ai/objectstack#23005's evidence states), rendered by @object-ui/app-shell's InterfaceListPage, which mounts ListView with viewType taken from appearance.allowedVisualizations[0]. This is read from source and was not measured on the cloud console: the cloud repository is not attached to this session. That page relays its own filter (publisher = 'private') and has no page-level emptyState to relay (the spec gap objectstack-ai/objectstack#23005). So it will draw the view-filter wording the grid draws on the same page: in zh 「没有匹配的记录」 / 「没有符合该视图筛选条件的记录。」.

RelatedList draws no card body while its list is empty (its !isEmpty guard), so its mobile gallery reaches the empty branch only in edge windows.

H2: what each caller can see

Input ListView ObjectGallery standalone
authored emptyState schema.emptyState (title, message, icon) none: ObjectGallerySchema declares no emptyState, and adding it is a type change this card rules out
view filter (hasBaseFilter) the resolved view filter its own filter, only when the gallery issued the query itself (fetchesForItself); rows a host handed in were filtered by the host
user query (hasUserQuery) search box, user-filter chips, filter panel none (false)
add-record action toolbarFlags.showAddRecord none: the gallery has no such control
translation and locale useListViewTranslation, useDisplayLocale the same two hooks

Chosen shape: ListView draws the shared component for gallery as it does for grid. That is the simplest correct shape, and ListView holds every input. ObjectGallery draws the component standalone with what it knows. The filter predicate is the shared hasDeclaredFilter, so the two callers cannot disagree about what a non-empty filter is.

H3: i18n

The copy uses the six list.* keys the grid already read: list.noMatches, list.firstRunTitle, list.noMatchesMessage, list.viewFilterNoMatchesMessage, list.firstRunMessage and list.addRecord. All six sit under list in the built-in zh pack, and the provider-less fallback is LIST_DEFAULT_TRANSLATIONS. ObjectGallery reaches them through ListView's own useListViewTranslation. No key is added; pnpm check:i18n-keys is green.

H4: other view types' empty branches (measured, not changed)

These are cited by file and quoted text, because AGENTS.md rule #11 bans line addresses in a pull-request body.

  • kanban, packages/plugin-kanban/src/KanbanImpl.tsx: the board-level DataEmptyState reads t('kanban.noCards'). It is localized, but it does not read the view's authored emptyState.
  • calendar, packages/plugin-calendar/src/ObjectCalendar.tsx: there is no empty-state copy; an empty calendar draws its grid.
  • timeline, packages/plugin-timeline/src/ObjectTimeline.tsx: there is no empty-state copy. The loading placeholder carries a hard-coded English screen-reader line, Loading timeline….
  • gantt, packages/plugin-gantt/src/ObjectGantt.tsx and GanttView.tsx: there is no empty-state copy; an empty chart draws its axis.
  • map, packages/plugin-map/src/ObjectMap.tsx: there is no empty-state copy over a working map. The WebGL-failure fallback list draws hard-coded English No locations to list., beside the hard-coded failure sentence that starts Map failed to load.
  • tree, packages/plugin-tree/src/ObjectTree.tsx: an empty tree draws hard-coded English No records (and Loading… while loading), although useTreeTranslation is in scope in that component.
  • chart, packages/plugin-charts/src/ObjectChart.tsx: a localized DataEmptyState through dashboard.empty.title (inline default No data yet).

Tests and gates (all at d7f71fd, this branch's head)

  • New pins, packages/plugin-list/src/__tests__/ObjectGallery.emptyState-12217.test.tsx, 10 rows:
    • Under ListView: zh first-run copy and the zh add-record label; an authored inline locale map painting its zh-CN entries; an authored plain string drawn as written; and the gallery and the grid drawing the same empty state node for node.
    • Standalone: zh first-run copy; built-in English copy with no add-record action; the view-filter wording when its own filter emptied its own query; and a no-filter control.
    • Two positive controls prove the gallery really is the visualization mounted.
  • Reverse verification. ListView.tsx and ObjectGallery.tsx were checked out at the base a491b64 (hash-equal to the base blobs; on disk No items to display = 1 and ListEmptyState = 0 in ListView.tsx). The new file then ran 8 failed, 2 passed: the eight rows that need the fix went red and the two controls stayed green, the direction predicted. Both paths were restored from HEAD under a trap and are hash-equal to the HEAD blobs, with git diff HEAD empty.
  • Controls the card names, unchanged and green: ListView.emptyVsErrorRole-7132, ListView.emptyStateFilteredCopy (objectui#4155, objectui#11687 rows), ListView.emptyStateI18nLabel-11227 and icon-seam-fallbacks-5935.
  • Targeted vitest, run from the repository root:
    • 40 plugin-list files (every ObjectGallery test, every ListView empty-state test, the gallery-touching ListView tests): Test Files 40 passed (40), Tests 457 passed (457).
    • 38 files outside the package that read ListView.tsx / ObjectGallery.tsx source or render a gallery through them. These include the InterfaceListPage relay census, the lucide icon census, the ObjectView gallery tests, RelatedList's mobile gallery and the defaults-maps-mirror-en-pack gate: Test Files 38 passed (38), Tests 845 passed (845).
    • The existing ObjectGallery.fetchGate-7903 negative assertion asserted on the old English string, so it would have passed for nothing. It now looks for the shared component's test id instead.
  • Builds and type-check. The dependency closure was built with turbo run build --filter='@object-ui/plugin-list^...' (12/12). pnpm --filter @object-ui/plugin-list run type-check echoed type-check and ran both tsc projects, exit 0. tsconfig.test.json's file list includes the new test.
  • Published face. dist/index.d.ts names none of ListEmptyState, useListViewTranslation or hasDeclaredFilter, and neither src/index.tsx nor package.json changed. The per-file declarations gain ListEmptyState.d.ts, and ListView.d.ts now exports useListViewTranslation. The exports map exposes only the package root, so neither is reachable through it.
  • Gates, exit 0: check:vi-mock-specifiers, check:test-path-roots, check:unreferenced-sources, check-changeset-presence (5 source files of 1 released package, 1 changeset), check:changeset-claims, check:pending-changeset-literals, check:control-bytes, check:new-line-citations (0 new citations). The derived gates are green too: check:icon-record-names, check:i18n-keys, check:i18n-dead-keys, check:i18n-drift, check:phantom-deps, check:unused-deps, check:vi-mock-inherit, check:vi-mock-override-shape and check:handler-key-reads.
  • Lint, narrowed and declared. eslint ran with the repository config on the 5 changed .ts/.tsx files (count read from --format json): 0 errors. The config extends tseslint.configs.recommended with no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move a verdict on an untouched file. The full eslint . run is CI's.
  • NOT MEASURED (each needs a console or all-package build this container did not make; CI runs them):
    • check:readme-exports: 386 self-imports unjudged because packages are unbuilt.
    • check:eager-closure: no apps/console/dist/eager-closure.json. The console aliases @object-ui/plugin-list to its src, whose barrel already imports both ListView and ObjectGallery, so the new ObjectGallery to ListView edge adds no module to any closure; ListEmptyState.tsx is moved code.
    • check:sdui-registration-pins: no console build. The object-gallery registration is unchanged.

Acceptance notes

  • Standalone galleries have no authored empty copy to show. ObjectGallerySchema declares no emptyState, and plugin-view ObjectView's own path (no renderListView) relays a named view's emptyState to no visualization, its grid included. Only its renderListView branch relays it. Not changed here: either half is a type or relay change outside this card.
  • The tree's No records and the map's No locations to list. are the same untranslated-empty-copy family as this card. They are listed in H4. Neither is filed here, and neither was measured at a public door.
  • Docs. No page under content/docs or packages/plugin-list/README.md describes the list's empty-state copy, so nothing went stale.

Generated by Claude Code

…slated

ObjectGallery drew a hard-coded English "No items to display" in every
locale and ignored the view's authored emptyState: ListView drew its
empty state for the grid only and handed an empty gallery the empty list.

The grid's empty-state block moves into one module-internal component,
ListEmptyState. ListView draws it for grid and gallery; ObjectGallery
draws it standalone with what it knows (no authored emptyState on the
node, no search box, user filter or add-record control; its own filter
counts only when it issued the query). The copy is the list.* keys the
grid already read, through ListView's own translation hook.

Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK
Co-authored-by: Claude <noreply@anthropic.com>
…ate takes the resolved glyph

The authored icon name is resolved through `resolveIcon` where it is read,
in ListView, and handed to ListEmptyState as a component; the `Inbox`
fallback stays in the shared component. ListView.tsx therefore still calls
the seam, as the lucide-icon census pins by path.

Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 70 chunks) 3127.0 KB 3204.6 KB
Main entry chunk (gzip) 73.1 KB 350 KB
Entry file index-TA5dXvDE.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.69KB 7.62KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 588.51KB 141.71KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 243.13KB 67.92KB
fields (index.js) 279.96KB 71.37KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.93KB 66.25KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 178.37KB 46.27KB
plugin-gantt (index.js) 178.63KB 45.19KB
plugin-grid (index.js) 255.09KB 70.91KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.35KB 31.54KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 11:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 11:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit f5b66bf Oct 12, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12217-gallery-empty-state branch October 12, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants