Repository navigation
Commit 7990fb0
fix(plugin-detail): remember "show empty fields" per user and object (#26560)
Fixes #25883
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:记录详情页每个分区的「显示 N
个空字段」不再是各分区各自、刷新即忘的开关,而是当前用户对该对象的一个选择:点一次,该对象详情页的所有分区都显示空字段,换一条同对象记录、刷新、换设备都保持;其它对象不受影响。存在
`sys_user_preference` 的 `ui.record.OBJECT` 行(值 `showEmptyFields:
true/false`),与列表页的 `ui.list.OBJECT` 同机制、同一次偏好读取。
- 为什么改:#25883 第 2 项。HotCRM 浏览巡检中,商机页三个分区要点三次,刷新或打开下一条商机又全部折叠。第 1
项(铅笔焦点显示)已在 #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](https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a14f3e6 commit 7990fb0
10 files changed
Lines changed: 770 additions & 50 deletions
File tree
- .changeset
- ui/packages
- app-shell/src
- console
- __tests__
- context
- __tests__
- plugin-detail/src
- __tests__
- react/src/context
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
48 | 53 | | |
49 | 54 | | |
50 | 55 | | |
| |||
194 | 199 | | |
195 | 200 | | |
196 | 201 | | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
227 | 234 | | |
228 | 235 | | |
229 | 236 | | |
| |||
350 | 357 | | |
351 | 358 | | |
352 | 359 | | |
| 360 | + | |
353 | 361 | | |
354 | 362 | | |
355 | 363 | | |
356 | 364 | | |
357 | 365 | | |
358 | 366 | | |
359 | 367 | | |
| 368 | + | |
360 | 369 | | |
361 | 370 | | |
362 | 371 | | |
| |||
384 | 393 | | |
385 | 394 | | |
386 | 395 | | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
387 | 403 | | |
| 404 | + | |
388 | 405 | | |
389 | 406 | | |
390 | 407 | | |
391 | 408 | | |
392 | 409 | | |
| 410 | + | |
393 | 411 | | |
394 | | - | |
| 412 | + | |
395 | 413 | | |
396 | 414 | | |
397 | 415 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
24 | 29 | | |
25 | 30 | | |
26 | 31 | | |
| |||
34 | 39 | | |
35 | 40 | | |
36 | 41 | | |
37 | | - | |
| 42 | + | |
38 | 43 | | |
39 | 44 | | |
40 | 45 | | |
| |||
87 | 92 | | |
88 | 93 | | |
89 | 94 | | |
| 95 | + | |
90 | 96 | | |
91 | 97 | | |
92 | 98 | | |
| |||
96 | 102 | | |
97 | 103 | | |
98 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
99 | 109 | | |
100 | 110 | | |
101 | 111 | | |
| |||
104 | 114 | | |
105 | 115 | | |
106 | 116 | | |
| 117 | + | |
107 | 118 | | |
108 | 119 | | |
109 | 120 | | |
110 | 121 | | |
111 | 122 | | |
112 | 123 | | |
| 124 | + | |
113 | 125 | | |
114 | 126 | | |
115 | 127 | | |
| |||
125 | 137 | | |
126 | 138 | | |
127 | 139 | | |
| 140 | + | |
| 141 | + | |
128 | 142 | | |
129 | 143 | | |
130 | 144 | | |
131 | 145 | | |
132 | 146 | | |
133 | 147 | | |
134 | 148 | | |
135 | | - | |
| 149 | + | |
136 | 150 | | |
137 | 151 | | |
138 | 152 | | |
139 | 153 | | |
140 | 154 | | |
141 | 155 | | |
142 | 156 | | |
| 157 | + | |
143 | 158 | | |
144 | 159 | | |
145 | 160 | | |
| |||
151 | 166 | | |
152 | 167 | | |
153 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
154 | 172 | | |
155 | 173 | | |
156 | 174 | | |
157 | 175 | | |
158 | 176 | | |
159 | 177 | | |
| 178 | + | |
160 | 179 | | |
161 | | - | |
162 | | - | |
| 180 | + | |
| 181 | + | |
163 | 182 | | |
164 | 183 | | |
165 | 184 | | |
| |||
186 | 205 | | |
187 | 206 | | |
188 | 207 | | |
189 | | - | |
| 208 | + | |
190 | 209 | | |
191 | 210 | | |
192 | 211 | | |
193 | | - | |
| 212 | + | |
194 | 213 | | |
195 | 214 | | |
196 | 215 | | |
| |||
207 | 226 | | |
208 | 227 | | |
209 | 228 | | |
210 | | - | |
| 229 | + | |
211 | 230 | | |
212 | 231 | | |
213 | 232 | | |
214 | 233 | | |
215 | 234 | | |
216 | 235 | | |
217 | 236 | | |
218 | | - | |
219 | | - | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
220 | 240 | | |
221 | 241 | | |
222 | 242 | | |
| |||
238 | 258 | | |
239 | 259 | | |
240 | 260 | | |
| 261 | + | |
241 | 262 | | |
242 | 263 | | |
243 | | - | |
244 | | - | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
245 | 267 | | |
246 | | - | |
| 268 | + | |
247 | 269 | | |
248 | 270 | | |
249 | 271 | | |
250 | 272 | | |
251 | | - | |
| 273 | + | |
252 | 274 | | |
253 | 275 | | |
254 | 276 | | |
| |||
259 | 281 | | |
260 | 282 | | |
261 | 283 | | |
| 284 | + | |
262 | 285 | | |
263 | 286 | | |
264 | 287 | | |
| |||
277 | 300 | | |
278 | 301 | | |
279 | 302 | | |
280 | | - | |
| 303 | + | |
281 | 304 | | |
282 | 305 | | |
283 | | - | |
| 306 | + | |
284 | 307 | | |
285 | 308 | | |
286 | 309 | | |
| |||
0 commit comments