Repository navigation
fix(plugin-detail): remember "show empty fields" per user and object - #26560
Conversation
…add the record-page preference seam Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude <noreply@anthropic.com>
… user and object The Console keeps it in the ui.record.<object> sys_user_preference row and hands it to the sections through @object-ui/react's RecordPagePreferencesProvider. With no store a section keeps its own toggle. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude <noreply@anthropic.com>
…ed show-empty choice Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude <noreply@anthropic.com>
维护者速读改了什么:记录详情页「显示 N 个空字段」从各分区各自、刷新即忘的开关,改为当前用户对该对象的一个选择:点一次,该对象详情页所有分区都显示空字段,换记录、刷新、换设备都保持;其它对象不受影响。存在 席位核对seat 2 · 2026-10-27T09:00Z · head
|
Fixes #25883
Clause-②: no
Metadata-protocol: none
维护者速读
sys_user_preference的ui.record.OBJECT行(值showEmptyFields: true/false),与列表页的ui.list.OBJECT同机制、同一次偏好读取。hideEmpty: true全空分区不会因该选择被显示出来(有测试)。控制台启动的那一次偏好读取多带一个ui.record.前缀分支,请求数仍是一次。回滚 = revert 本 PR,已写入的ui.record.*行无人读取、无害。ui/packages/plugin-detail/src/DetailSection.tsx的rememberedShowEmpty,以及ui/packages/app-shell/src/context/RecordPreferencesProvider.tsx。plugin-detail 经@object-ui/react的新 context 读取,不依赖 app-shell,未新增包依赖。What changed
The record page's "Show N empty fields" row is now one remembered choice per user and object, instead of a per-section toggle that reset on unmount.
Storage key:
ui.record.OBJECT(onesys_user_preferencerow per object, for exampleui.record.opportunity), value{ "showEmptyFields": true }orfalse. It is a new key family beside the list page'sui.list.OBJECT(objectui#12418), and it uses the same mechanism:createObjectStackUserPreferenceFamily, attached by the console'sUserStateBridge, loaded by prefix in the console's one shared preference read, and each object's row upserted on its own. I chose a separate family over a new field insideui.list.OBJECTbecause that row is documented and sanitized as list state; record-page state there would be dropped bysanitizeObjectListPreferenceand would blur what the row means.data-objectstack's shared read already takes several prefixes, souserState.tsis unchanged.ui/packages/react/src/context/RecordPagePreferencesContext.tsx(new):RecordPagePreferencesProvider,useRecordPagePreferences, and theRecordPagePreference/RecordPagePreferencesStoretypes. This is the seam to the record page. The sections reach the page throughComponentRegistry, so no host prop reaches them. A context in@object-ui/reactdoes reach them, asHostNavigationContextandHostMapStyleContextalready do for their renderers. plugin-detail and app-shell both already depend on@object-ui/react, so no package dependency was added and plugin-detail does not import app-shell.ui/packages/plugin-detail/src/DetailSection.tsx: under a store and with anobjectName, the toggle reads and writes the store'sshowEmptyFieldsfor that object, so every section of the object follows one choice. With no store or no object name, it keeps its ownuseState, exactly as before. The authored all-empty hide (hideEmpty: true) now applies regardless of the choice. Before, the in-memory toggle could never be on for such a section because it never drew the toggle. An object-wide choice can reach it, so the hide is now applied explicitly.ui/packages/app-shell/src/context/RecordPreferencesProvider.tsx(new): loads the family's rows on attach (a user switch drops the previous user's choices), keeps a choice made before the read answers over the stored row, and writes one row per click. With no family attached it hands the page no store (null), so the old in-memory behaviour applies. There is no localStorage copy.ui/packages/app-shell/src/context/UserStateAdapters.tsx: arecordStatefamily slot withuseRecordStateFamilyanduseAttachRecordStateFamily, mirroringlistState.ui/packages/app-shell/src/console/ConsoleShell.tsx: the bridge builds and attaches theui.record.family next toui.list., andRecordPreferencesProvideris mounted insideListPreferencesProvider. Most of this file's diff is a two-space re-indent of the nested children..changeset/25883-empty-fields-remembered.md:@object-ui/react,@object-ui/plugin-detail,@object-ui/app-shellpatch.Not changed: locale packs (the existing
detail.showEmptyFields/detail.hideEmptyFieldsstrings are reused),HeaderHighlight.tsx,RelatedList.tsx,ListPreferencesProvider.tsx, anddata-objectstack. No registry component contract changed, so no SDUI manifest regeneration was needed.Tests
ui/packages/plugin-detail/src/__tests__/DetailSection.emptyFieldsRemembered-25883.test.tsx(7 tests) pins these cases: toggle, remount, still shown; the object's other sections follow; another object is unaffected; the store receivesshowEmptyFields: truethenfalse; a stored choice opens shown; no store means per-section toggles that are gone on unmount (the old behaviour); noobjectNamemeans the section's own toggle; a remembered choice does not undohideEmpty: true.ui/packages/app-shell/src/context/__tests__/RecordPreferencesProvider-25883.test.tsx(5 tests), with the realDetailSectionunder the real provider and a fake family. Toggle, remount with the reloaded row, still shown. The other sections follow, another object is unaffected, andui.record.opportunityis written. With no family, the hook answersnulland each section keeps its own toggle. The provider reads onlyui.record.rows, sanitized. A choice made before the read answers is kept. A user switch drops the previous user's choices.ui/packages/app-shell/src/console/__tests__/consoleUserPreferenceBudget.test.tsx(realConsoleShellandConnectedShell, fake preference server): one boot is still ONEsys_user_preferenceGET, and it now asks for both prefixes. The record-page consumer receives its ownui.record.accountrow, writes only that row, and a user switch reads the new user's row.1b034058(the seam and the plugin-detail pin committed, the DetailSection change absent):Tests 4 failed | 2 passed (6). The four failures are the remount, all-sections, store-write and stored-choice cases. The two that pass are the no-store and no-object-name cases, which are the old behaviour by design. This leg ran without the verify lock after three 540 s queue timeouts (one file,--maxWorkers=1), with the backend packages' builtdist/copied from the same base.2646abdc, underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0):pnpm --filter '@object-ui/plugin-detail^...' --filter '@object-ui/app-shell^...' build: exit 0;pnpm exec vitest run --maxWorkers=2 packages/plugin-detail(cwdui/):Test Files 310 passed | 1 skipped (311),Tests 2876 passed | 18 skipped (2894). 311 equals the test files on disk;ConsoleShell,UserStateAdaptersor the new provider (not the full app-shell suite):Test Files 25 passed (25),Tests 201 passed (201);pnpm --filter @object-ui/plugin-detail run type-check,pnpm --filter @object-ui/react run type-checkandpnpm --filter @object-ui/app-shell run type-check(each runstsc --noEmitandtsc -p tsconfig.test.json): all exit 0.2646abdc, fromnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 derived.--ranreconciliation: 33 run, 1 NOT-MEASURED, 0 UNRUN.check-sdui-manifest.mjs,check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:type-check-coverage,check:undeclared-dep-importsand the changeset gates.pnpm --dir ui exec vitest run scripts/exited 1: 5094 passed and 3 failed, all three on the 15 s test timeout on a contended box. Those three files (check-hand-rolled-comment-mask,check-vi-mock-inherit,js-comment-mask-returns-interpolation-9424) were then re-run on their own:Tests 113 passed (113).pnpm check:type-check-debtis NOT-MEASURED. Its re-measure needs a closure build on the shared verify lock, which timed out twice in the queue. CILint & Repo Gatescarries it.Acceptance notes
EmptySectionsFold, shown when every section of a body is empty) keeps its own in-memory reveal. It was outside this claim's file surface. Under a remembered "show", revealing it draws the sections with their empty fields already shown, and their "Hide empty fields" rows clear the choice. Whether that fold should read the same choice is a follow-up question, not a defect.Generated by Claude Code