Repository navigation
fix(app-shell,plugin-list,i18n): list empty states match the page they sit on (objectui#11687) - #11727
Conversation
…jectui#11687) The better-auth empty state yields to the list's own copy when the page offers a way to add a row (its New button, or a list_toolbar action the toolbar draws). The list's empty state tells the view's own filter apart from a user filter or search, with a new list.viewFilterNoMatchesMessage key in every locale pack. Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z Co-authored-by: Claude <noreply@anthropic.com>
…re flag (objectui#11687) Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z Co-authored-by: Claude <noreply@anthropic.com>
…tui#11687) Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsInputs, read once at 2026-10-06T15:48Z with no polling: card objectui#11687 (body and its three comments: the claim, the os-dev-report, the in-seat ACCEPT); PR objectui#11727 (body, the 17-file list, the net diff against Every accept-set and public-surface change the diff implies, each judged:
Check-runs on the head at the read: 42. 29 ② Semver levelChangeset
③ Boundary flagsDev
Dev
Dev
Nothing escalates to the maintainer. The unconverged check families named in ① are for the enqueue leg to see green before landing; they are not a contract question. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Fixes #11687
Clause-②: yes
Implemented by the os-dev agent dispatched from seat
domain:ui#1, sessionhttps://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z, under the claim on the card (comment 6017757987).What was wrong
The two empty-state defects measured on the console (objectui#11672, item 15):
Mechanism, measured against the PM's hypotheses
better-autharm ofresolveManagedByEmptyStatereturned the identity copy for every object exceptsys_user/sys_team, without asking anything about the page. Theengine-ownedarm already yields when thecreateaffordance is open.list_toolbar. They are not the CRUDcreateaffordance:sys_invitationandsys_oauth_applicationleavecreateat the bucket default, which is off. Their gates reach the client already lowered intovisible(requiresFeatureandrequiresMembershipReach), so whether the button is on the page depends on the deployment and the caller. A predicate onresolveEffectiveCrudAffordancesalone would have left both pages broken while the tests passed. Onlycreate_oauth_applicationdeclaresmode: 'create'.invite_userdeclares no mode, so amode-based predicate would also have missed Invitations.filterstill counts as a query, for objectui#4155's reason: a list emptied by its view filter must not get the first-run copy.sys_api_keydeclares nolist_toolbaraction and keepscreateat the bucket default. Keys are minted throughPOST /api/v1/keys. This holds at the dogfood's framework commitce577ec4and at objectstackaa09db58. The sweep's sentence put three pages under two button names. API Keys is therefore pinned as the page whose identity copy stays.The change
@object-ui/app-shell:ObjectViewcomputespageOffersCreate. It is true when the page's New button shows (objectCanCreate && createVisible) or when the toolbar draws alist_toolbaraction. The toolbar half is the newlistToolbarDrawsActioninutils/managedByEmptyState.ts. It asks the same three questionsaction:barand thenaction:buttonask:actionRendersAt(action, 'list_toolbar'), theuseCapabilityGateclosure, and thevisiblegate. The gate goes throughhasDeclaredPredicateand is evaluated fail-closed with no row bound.resolveManagedByEmptyStatetakes the boolean as a fifth argument. In thebetter-autharm, a page that offers a create action falls back to the list's own empty-state copy.sys_teamkeeps its copy, which already names its Create Team button, and stays ahead of the check. The package entry exports neither function, so the signature change does not reach the published surface.@object-ui/plugin-list:ListViewnow chooses its empty-state message three ways:list.noMatchesMessage;list.viewFilterNoMatchesMessage, "No records match this view’s filter.", under the existing "No matching records" title;The key is also in
LIST_DEFAULT_TRANSLATIONS.@object-ui/i18n:list.viewFilterNoMatchesMessageis added to all ten packs.Changeset
.changeset/11687-empty-state-copy-matches-page.md:minorfor i18n and plugin-list, whose published surface grows, andpatchfor app-shell.Pins: the four measured pages
packages/app-shell/src/views/ObjectView.emptyStateMatchesPage-11687.test.tsxmounts the realObjectViewand the realListViewinside a real i18next instance booted in en. Each object is in its served shape, with gates already lowered:all_keys): the page offers no create action, so the identity copy stays.all_apps): Register OAuth Application is drawn, and the list shows the first-run copy.Also added: unit cases in
managedByEmptyState.test.ts(the resolver andlistToolbarDrawsAction) and three cases inListView.emptyStateFilteredCopy.test.tsx(view filter only; a typed search; a user filter over a filtered view).Reverse verification
These were one-off runs; each direction was predicted before running. Each leg changed the committed fix through
ablation-replace.mjs: the anchor went from 1 hit to 0 and the blob changed. The leg then ran the suites and restored the file, proven byblob == HEADand an emptygit diff HEAD. Tests resolve@object-ui/*tosrcthrough the root vitest alias, so no rebuild was needed.pageOffersCreatecheck removed: 4 failed, 29 passed. The two resolver unit cases, Invitations and OAuth failed. Both controls and Webhooks stayed green.hasActiveQuerymessage choice restored: 3 failed, 30 passed. The ListView view-filter case, Invitations and Webhooks failed.listToolbarDrawsActionno longer readsvisible: 3 failed, 23 passed across two suites. The two gate unit cases and the single-org control failed.Local verification at
0ef8b17pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' --filter '@object-ui/i18n^...' build, exited 0 (29 of 47 workspace projects).type-checkfor@object-ui/i18n,@object-ui/plugin-listand@object-ui/app-shell: all exit 0. Each script ran astsc --noEmit && tsc -p tsconfig.test.json, and the test project includessrc/**/*.test.ts(x).test, i18n: 80 files, 1304 passed and 13 skipped. plugin-list: 113 files, 1224 passed. app-shell: 1032 files passed and 1 skipped, 10181 tests passed and 9 skipped.check:control-bytes,check:new-line-citations,check:changeset-claims,check:pending-changeset-literals,check:i18n-keys,check:i18n-drift,check:i18n-dead-keys(report only; the new key is not a candidate),check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:unreferenced-sources, and the changeset presence, no-major, fixed and overwrite scripts: all exit 0.check:eager-locale-catalogues. It printedPREREQUISITE NOT METbecause it needs a built console bundle. That run belongs to CI..ts/.tsxfiles (the--format jsoncount) and found 0 errors. The 366 warnings are pre-existing rules onObjectView.tsx/ListView.tsx; none is on an added line.eslint.config.jssets noparserOptions.project/projectService, so linting is not type-aware and this diff cannot change the verdict on any untouched file. The repo-widepnpm lintbelongs to CI.Acceptance notes (observations; none filed)
ListViewreadsschema.emptyStatebefore its own choice, and the objectui#4155 pin "the author's own emptyState copy still wins over both" holds that order. On abetter-authlist whose page offers no create action, such as API Keys or single-org Invitations, a search that matches nothing therefore shows "No identity records" rather than "No matching records". This comes from reading the source; it was not measured in the running app. Carrier: none.featureGatePredicatedoc says a default-on flag fails open when it is absent ("e.g. config not yet fetched"). Measured here:features.organization != falseevaluated againstfeatures: {}throws in objectui's CEL evaluator, and the fail-closedvisiblehides the action. The server may always report every flag, so this was not measured in the running app. Seam:spec:featureGatePredicate → renderer:action-button. Carrier: none.sys_team's copy names "Create Team" even where that action is hidden: its gate is the organization feature plus owner/admin reach. This predates the PR and is unchanged.ObjectViewfolds URL filters (?filter=deep links) into the view'sfilter, so an empty deep-linked list now reads the view-filter message.content/docs, and none of the three package READMEs, describes how the list chooses its empty state.Generated by Claude Code