Skip to content

fix(plugin-detail): remember "show empty fields" per user and object - #26560

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-25883-empty-fields-remembered
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-25883-empty-fields-remembered

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25883
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:记录详情页每个分区的「显示 N 个空字段」不再是各分区各自、刷新即忘的开关,而是当前用户对该对象的一个选择:点一次,该对象详情页的所有分区都显示空字段,换一条同对象记录、刷新、换设备都保持;其它对象不受影响。存在 sys_user_preference 的 ui.record.OBJECT 行(值 showEmptyFields: true/false),与列表页的 ui.list.OBJECT 同机制、同一次偏好读取。
  • 为什么改:record page read mode: inline edit exists only as an invisible "双击编辑" affordance, and each section's "显示 N 个空字段" toggle is separate and forgets its state on reload #25883 第 2 项。HotCRM 浏览巡检中,商机页三个分区要点三次,刷新或打开下一条商机又全部折叠。第 1 项(铅笔焦点显示)已在 fix(plugin-detail): reveal the inline-edit pencil on keyboard focus #26325 落地。
  • 风险与回滚:没有偏好服务(未登录、宿主未挂桥接)时行为与原来完全一致(各分区内存态)。作者写的 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,未新增包依赖。
  • 两分钟验证:登录控制台打开一条有空字段的商机,点任一分区的「显示 N 个空字段」,其余分区同时展开;刷新、打开另一条商机仍展开;打开客户记录仍折叠。
  • 需要你做的:无。不涉及受管面,CI 绿后由席位按常规合并队列落地。

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 (one sys_user_preference row per object, for example ui.record.opportunity), value { "showEmptyFields": true } or false. It is a new key family beside the list page's ui.list.OBJECT (objectui#12418), and it uses the same mechanism: createObjectStackUserPreferenceFamily, attached by the console's UserStateBridge, 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 inside ui.list.OBJECT because that row is documented and sanitized as list state; record-page state there would be dropped by sanitizeObjectListPreference and would blur what the row means. data-objectstack's shared read already takes several prefixes, so userState.ts is unchanged.

  • ui/packages/react/src/context/RecordPagePreferencesContext.tsx (new): RecordPagePreferencesProvider, useRecordPagePreferences, and the RecordPagePreference / RecordPagePreferencesStore types. This is the seam to the record page. The sections reach the page through ComponentRegistry, so no host prop reaches them. A context in @object-ui/react does reach them, as HostNavigationContext and HostMapStyleContext already 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 an objectName, the toggle reads and writes the store's showEmptyFields for that object, so every section of the object follows one choice. With no store or no object name, it keeps its own useState, 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: a recordState family slot with useRecordStateFamily and useAttachRecordStateFamily, mirroring listState.
  • ui/packages/app-shell/src/console/ConsoleShell.tsx: the bridge builds and attaches the ui.record. family next to ui.list., and RecordPreferencesProvider is mounted inside ListPreferencesProvider. 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-shell patch.

Not changed: locale packs (the existing detail.showEmptyFields / detail.hideEmptyFields strings are reused), HeaderHighlight.tsx, RelatedList.tsx, ListPreferencesProvider.tsx, and data-objectstack. No registry component contract changed, so no SDUI manifest regeneration was needed.

Tests

  • New 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 receives showEmptyFields: true then false; a stored choice opens shown; no store means per-section toggles that are gone on unmount (the old behaviour); no objectName means the section's own toggle; a remembered choice does not undo hideEmpty: true.
  • New ui/packages/app-shell/src/context/__tests__/RecordPreferencesProvider-25883.test.tsx (5 tests), with the real DetailSection under the real provider and a fake family. Toggle, remount with the reloaded row, still shown. The other sections follow, another object is unaffected, and ui.record.opportunity is written. With no family, the hook answers null and each section keeps its own toggle. The provider reads only ui.record. rows, sanitized. A choice made before the read answers is kept. A user switch drops the previous user's choices.
  • Extended ui/packages/app-shell/src/console/__tests__/consoleUserPreferenceBudget.test.tsx (real ConsoleShell and ConnectedShell, fake preference server): one boot is still ONE sys_user_preference GET, and it now asks for both prefixes. The record-page consumer receives its own ui.record.account row, writes only that row, and a user switch reads the new user's row.
  • Red first, at 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' built dist/ copied from the same base.
  • Green, at head 2646abdc, under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0):
    • closure build pnpm --filter '@object-ui/plugin-detail^...' --filter '@object-ui/app-shell^...' build: exit 0;
    • pnpm exec vitest run --maxWorkers=2 packages/plugin-detail (cwd ui/): Test Files 310 passed | 1 skipped (311), Tests 2876 passed | 18 skipped (2894). 311 equals the test files on disk;
    • the 25 app-shell test files that import or read ConsoleShell, UserStateAdapters or 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-check and pnpm --filter @object-ui/app-shell run type-check (each runs tsc --noEmit and tsc -p tsconfig.test.json): all exit 0.
  • Gates, at 2646abdc, from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 derived. --ran reconciliation: 33 run, 1 NOT-MEASURED, 0 UNRUN.
    • 32 exited 0, including check-sdui-manifest.mjs, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:undeclared-dep-imports and 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-debt is NOT-MEASURED. Its re-measure needs a closure build on the shared verify lock, which timed out twice in the queue. CI Lint & Repo Gates carries it.

Acceptance notes

  • The tab-level fold (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.
  • Until the boot's preference read answers, a section draws folded. The read is sent at boot beside the metadata reads, so it has normally answered before a record page renders.

Generated by Claude Code

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:记录详情页「显示 N 个空字段」从各分区各自、刷新即忘的开关,改为当前用户对该对象的一个选择:点一次,该对象详情页所有分区都显示空字段,换记录、刷新、换设备都保持;其它对象不受影响。存在 sys_user_preference 的 ui.record.OBJECT 行,与列表页 ui.list.OBJECT 同机制、同一次偏好读取。
为什么改:#25883 第 2 项:商机页三个分区要点三次,刷新或打开下一条商机又全部折叠。第 1 项已在 #26325 落地。
风险与回滚:没有偏好服务(未登录、宿主未挂桥接)时行为与原来一致(各分区内存态);作者声明的全空隐藏不会因这个选择被显示出来(有测试);控制台启动的偏好读取仍是一次请求。回滚 = revert 本 PR,已写入的 ui.record.* 行无人读取、无害。
先看:ui/packages/plugin-detail/src/DetailSection.tsx 的 rememberedShowEmpty → ui/packages/react/src/context/RecordPagePreferencesContext.tsx → ui/packages/app-shell/src/context/RecordPreferencesProvider.tsx 与 ConsoleShell.tsx 的挂载处。
两分钟验证:在 ui/ 下运行 pnpm exec vitest run packages/plugin-detail/src/__tests__/DetailSection.emptyFieldsRemembered-25883.test.tsx packages/app-shell/src/context/__tests__/RecordPreferencesProvider-25883.test.tsx,预期全过。
需要你做的:无,席位落地。
席位意见:ACCEPT。接缝放在 @object-ui/react 的新 context,plugin-detail 不依赖 app-shell,没有新增包依赖。整页全空时那一行折叠(EmptySectionsFold)仍是内存态,不在本卡范围,记为后续。

席位核对

seat 2 · 2026-10-27T09:00Z · head 2646abdc (#25883 item 2)

item reading
form 10 files, +770/−50: DetailSection.tsx reads/writes showEmptyFields for objectName through useRecordPagePreferences (falls back to its own state with no store or no object name), and the authored all-empty hide now holds whatever the choice; new @object-ui/react RecordPagePreferencesContext (shaped like HostNavigationContext); app-shell RecordPreferencesProvider, a recordState slot on UserStateAdapters, the ui.record. family mounted in ConsoleShell inside ListPreferencesProvider (~70 lines are re-indent); two pins; consoleUserPreferenceBudget restated for the second prefix in the same one read; root .changeset/ patch on react, plugin-detail, app-shell
premise measured on main the toggle was per-section useState; data-objectstack's shared preference read already takes several prefixes, so userState.ts is unchanged; ui.list.OBJECT is not reused because sanitizeObjectListPreference drops non-list fields
gates (check-runs on 2646abdc) 45 success, 9 skipped, 0 failing (Test UI 16/16, Build & E2E UI, Type Check UI and debt ledger, Lint UI, Check PR Size, governed guard all green). Local: red 4/6 at 1b034058 (seam + pin, DetailSection unchanged; the two passing cells are the no-store / no-objectName old behaviour), green after; plugin-detail 310 files / 2876 tests; 25 app-shell files / 201 tests; type-check exit 0 for react, plugin-detail, app-shell; check-sdui-manifest exit 0; derived gates 33 run (32 exit 0; ui scripts/ vitest 3 timeouts under load, the 3 files re-run alone 113/113), check:type-check-debt not measured locally, left to CI
accepted deviations red leg run outside the lock after three lock timeouts (single file, --maxWorkers=1); file surface wider than the claim named (react context + index export, app-shell UserStateAdapters.tsx, ConsoleShell.tsx), within the claim's "preference seam" clause; the seventh cell (hideEmpty guard) is a regression guard added with the fix, not a red-first pin
out of scope EmptySectionsFold (the page-level all-empty line) stays in-memory; follow-up only if it is reported as a gap
landing not governed (check-governed-merges: 820 changed lines, no hit paths); git merge-tree against origin/main 1080f07 clean; no other open PR touches DetailSection.tsx, ConsoleShell.tsx, UserStateAdapters.tsx or the react context index; Fixes #25883 closes the card (item 1 landed in #26325)

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 09:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 09:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 7990fb0 Oct 27, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25883-empty-fields-remembered branch October 27, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

2 participants