Repository navigation
Commit 9dacccf
feat(app-shell,plugin-detail,components,i18n): the record page's timeline and related columns are each reader's per-object switches, toggled from the crumb row (#27120)
Fixes #27052
Clause-②: no
Metadata-protocol: none
## 维护者速读
-
**改了什么**:记录页的「时间线」「关联」两列变成读者自己的开关,按对象记住;默认值从对象推导(被工作的对象开时间线,否则关),不是元数据。crumb
行右侧多了一组图标开关(☴ 时间线 ⌘⇧T、⊞ 关联 ⌘⇧R),关掉时间线时它的内容折进关联列底部的「动态」卡。
- **为什么改**:维护者裁决「时间线是否显示,是否可以切换……顶部有很多切换图标」——产品、价格表这类对象的中间列是一条空的流和一个输入框。
- **风险与回滚**:只动前端(app-shell / plugin-detail / components / i18n),无
spec、无元数据键;回滚即 revert 本 PR。偏好存在浏览器 localStorage,回滚后旧键残留无害。
-
**先看**:`ui/packages/plugin-detail/src/synth/recordPageColumns.ts`(默认值推导与排布规则)、`ui/packages/app-shell/src/views/RecordDetailView.tsx`(crumb
行与快捷键)、`ui/packages/app-shell/src/utils/workspacePreference.ts`(偏好存储)。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/RecordDetailView.panelToggles-27052.test.tsx`,预期
6 passed;或看下方证据分支的三张截图。
- **需要你做的**:无,席位已裁两条 open questions(见下方 Resolved)并落地。
## What changes
- **One workspace-preference store
(`app-shell/src/utils/workspacePreference.ts`).** A preference is
declared once and read per scope: `user`, or `object:OBJECT_NAME`. Each
preference and scope pair has one `localStorage` key. The value is a
small JSON record carrying `v`, and a record of another version reads as
"no choice". Reads are live through `useSyncExternalStore`, other tabs
arrive through the `storage` event, and with no storage (SSR, a blocked
origin, a refused write) a read falls back to the default while a
refused choice is held for the page.
- **`recordOpenPreference` is a thin alias over it.** Its key
`os:record-open-mode` and its bare `drawer` / `split` spelling are
unchanged, so a reader's earlier choice reads back and the record-open
preference tests pass unchanged.
- **Two new preferences.** `record.columns.timeline` and
`record.columns.related` take `on` / `off` per object. With no choice,
`recordPageColumnDefaults` derives the default: the timeline is on for a
worked object and off otherwise, and the related column is on.
`recordTimelineDefaultReason` names the branch: `stageField` names a
field, a field's options declare a lifecycle `kind` (unless `stageField:
false`), or `enable.files: true`. Being the `master_detail` parent of
another object is not a signal (seat ruling 1, PR comment 6464903026): a
container that owns child rows is not a worked record. The derivation
reads only the object itself, so `recordTimelineDefaultReason` /
`recordPageColumnDefaults` take no object list.
- **The column tiers read the switches.**
`recordPageArrangementFor(tier, switches)` is the rule, and
`RecordPageColumns` takes `switches`. With the timeline off, a wide page
draws details | related and the timeline's blocks become an Activity
card at the foot of the related column. With related off, the related
column stays behind the existing rail toggle. Below the column widths
the page is tabbed as before, the switches are kept, and `?tab=` is
untouched. Authored `kind: full` pages and `slots.tabs` never reach this
code.
- **`PanelToggles` (`@object-ui/components`).** It is a named group of
icon toggles with `aria-pressed`, a tinted pressed state and a tooltip
showing the shortcut's keycaps. The related switch is named
`detail.relatedColumn` (「关联列」), apart from the rail toggle and the
Related tab, so the row never carries two controls named 「关联」. The
record page draws it on its crumb row, right side, only in a column
tier. It is not drawn in the drawer or split pane.
- **Shortcuts.** `⌘⇧.` / `Ctrl+Shift+.` (timeline) and `⌘⇧,` /
`Ctrl+Shift+,` (related) flip the switches while the full record page is
in a column tier — matched by physical key (`KeyboardEvent.code`
`Period` / `Comma`), since Shift changes the character those keys type.
The card's `⌘⇧T` / `⌘⇧R` are browser-reserved (reopen closed tab, hard
reload) and were replaced by seat ruling 2; a `⌘⇧T` / `⌘⇧R` keydown now
leaves the columns and the stored preference untouched. They do nothing
while a text field has focus, and they are advertised to the
keyboard-shortcuts dialog under the same gate.
- **i18n.** `detail.timeline`, `detail.timelineFolded`,
`detail.relatedColumn`, `detail.panelToggles`,
`console.shortcuts.toggleTimeline` and `console.shortcuts.toggleRelated`
are added in all ten packs; no pack names a chord.
### Merge order with the record-page navigation PR
The navigation PR for the record page, #27038,
adds ↑ ↓ and a back link to the same crumb row. This PR does not depend
on it. Whichever lands second places its controls so the row reads ↑ ↓ ·
divider · panel toggles. The first to land does not wait for the other.
## Tests
- `workspacePreference.test.ts` covers keys per preference and scope,
the versioned record, unreadable spellings, no storage and SSR.
`workspacePreference.hook.test.tsx` covers the live hook: same-scope
callers re-render, scopes stay apart, cross-tab `storage` events arrive,
and a refusing storage holds the choice for the page.
- `recordPageColumns.switches-27052.test.ts` has one row per derivation
branch and per arrangement case.
`RecordPageColumns.switches-27052.test.tsx` checks where each block
lands when a column is off.
- `panel-toggles.test.tsx` covers the group name, `aria-pressed`, the
tint class, click behaviour and the tooltip keycaps.
- `RecordDetailView.panelToggles-27052.test.tsx` mounts the real view.
It covers the card's acceptance 1: timeline off on a worked object draws
details | related with the folded card, and on restores it. The choice
survives a remount and is per object. An object that is not worked opens
with the timeline off. `⌘⇧T` with an input focused does nothing. Nothing
is drawn in the tabbed tier or when embedded.
- `recordOpenPreference.test.ts` and the two `ObjectView` record-open
suites pass unchanged (acceptance 3).
## Acceptance notes
- **Screenshots (acceptance 2).** They were taken on HotCRM `f660fd8`,
fresh DB, with this branch's console at 1720 px wide. HotCRM's
`crm_lead` carries an authored `kind: 'full'` record page, which the
card leaves untouched, so the column tiers never draw there. The
worked-object shots therefore use `crm_contact`, which has
`enable.files: true` and no authored page. Evidence branch (never
merge): `claude/issue-27052-evidence`, folder `evidence-27052/`.
- `1-contact-both-columns.png`: three columns and both toggles pressed.
- `1b-contact-toggle-tooltip.png`: the tooltip with its keycaps.
- `2-contact-timeline-off.png`: details | related.
- `3-product-default.png`: `crm_product` opens as details | related,
with the timeline folded at the foot of the related column.
- A browser readback on that run confirmed three more things. The
contact's choice survived a reload. `Ctrl+Shift+T` sent by Playwright
turned the timeline back on (that run predates ruling 2; the chord is
now `Ctrl+Shift+.`, pinned in `panelToggles-27052`, not re-measured in a
real browser: Playwright injects keys below the browser's accelerator
layer). The root measured 1464 px.
- **The drawer width is not yet read through the store.** The card asked
for the `ov:drawer-width:OBJECT` key to go through the store too. That
read and write live in `navigation-overlay.tsx`, which this dispatch
fences off because the navigation PR edits it. It is left for after that
PR lands.
- **⌘K.** The command palette on main has no command registry.
`CommandPaletteProvider` only opens and closes it, and `CommandPalette`
hard-codes its groups. So this PR adds the shortcuts and no palette
commands.
- **Two private helpers repeated.** The chord matcher and the text-field
check repeat `AppHeader`'s, which are private to that file. Sharing them
would edit a file outside this card's surface.
- **New preferences only carry `v`.** The record-open preference keeps
its bare stored value without `v`, because its tests pin the stored
bytes.
### HotCRM reading of the default derivation
Read with `recordPageColumnDefaults` over the 20 object definitions of
HotCRM `f660fd8`, loaded from source, on head `fd0f941f70` (after ruling
1):
| object | timeline | branch |
|:---|:---|:---|
| crm_lead, crm_opportunity, crm_task, crm_case | ON | option-kinds |
| crm_account, crm_contact, crm_contract, crm_quote | ON | files |
| crm_campaign, crm_price_book, crm_event, crm_knowledge_article,
crm_product, crm_forecast, crm_opportunity_line_item,
crm_quote_line_item, crm_price_book_entry, crm_campaign_member,
crm_event_attendee, crm_article_feedback | OFF | none |
8 ON / 12 OFF; the related column defaults to ON for all 20. This
matches the card's expected list exactly. Before ruling 1 the four
container objects (campaign, price book, event, knowledge article) were
ON through a `master_detail`-parent branch that is now gone.
### Open questions — resolved by the seat (PR comment 6464903026), both
landed in patch round 1
1. **The master_detail-parent branch: B, dropped.** A container that
owns child rows is not a worked record (ruling 2 on the card); every
object the card expects ON stays ON through stage kinds or files; the
rule reads two declared signals of the object itself. Pinned by a
view-level case (an object that is only another object's `master_detail`
parent opens with the timeline off) and the HotCRM table above.
2. **Chords: B, moved off the browser's.** `⌘⇧.` / `⌘⇧,` (`Ctrl+Shift+.`
/ `Ctrl+Shift+,`), matched by physical key; tooltip keycaps,
`aria-keyshortcuts` and the shortcuts dialog read the same constants.
Delivery through a real browser's accelerator layer is NOT MEASURED on
this host (Playwright and devtools inject below it); the page-side
behaviour is pinned.
## Verification
Final head `fd0f941f70`. Cloud run: report comment 6464041380 on the
card (34 / 34 derived gates exit 0 on `b36136e2`, ablation of the
typing-target guard). Patch round 1 (local, user-token pushes): report
comment 6466430387 (34 / 34 gates on `a6d6ac6fb4` with the list
re-derived identical on `fd0f941f70`; 634 files / 6348 tests across
plugin-detail, i18n, components custom, app-shell utils / chrome /
RecordDetailView* / ObjectView*; plugin-detail + app-shell type-check
with their test tsconfigs; two reverse verifications with blob hashes
and a clean `git diff HEAD`). CI on `fd0f941f70`: Test UI 17 / 17
shards, Type Check UI, Lint UI, Build & E2E UI all success.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q4EjjR2sbXvZiQ3HsgSBhb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>1 parent 4265176 commit 9dacccf
27 files changed
Lines changed: 1786 additions & 167 deletions
File tree
- .changeset
- ui/packages
- app-shell/src
- utils
- views
- components/src/custom
- i18n/src/locales
- plugin-detail/src
- __tests__
- synth
- __tests__
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
10 | 16 | | |
11 | 17 | | |
12 | 18 | | |
| |||
34 | 40 | | |
35 | 41 | | |
36 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
37 | 50 | | |
38 | 51 | | |
39 | 52 | | |
| |||
49 | 62 | | |
50 | 63 | | |
51 | 64 | | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
61 | 78 | | |
62 | 79 | | |
63 | 80 | | |
| |||
66 | 83 | | |
67 | 84 | | |
68 | 85 | | |
69 | | - | |
| 86 | + | |
70 | 87 | | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
| 88 | + | |
78 | 89 | | |
79 | 90 | | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | 91 | | |
94 | 92 | | |
95 | 93 | | |
| |||
103 | 101 | | |
104 | 102 | | |
105 | 103 | | |
106 | | - | |
| 104 | + | |
107 | 105 | | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
| 106 | + | |
142 | 107 | | |
143 | 108 | | |
144 | 109 | | |
| |||
148 | 113 | | |
149 | 114 | | |
150 | 115 | | |
151 | | - | |
152 | | - | |
153 | | - | |
| 116 | + | |
154 | 117 | | |
| 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 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 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 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
0 commit comments