Repository navigation
Commit 34c2407
feat(app-shell,components): the record drawer opens a related row or a lookup in place, one level at a time, and its bar names no object (#27038)
Fixes #26679
Clause-②: no
Metadata-protocol: none
## What changes
The console's record drawer is one drawer, and a record opened from
inside it opens in place (maintainer rulings 2-5 on the card, quoted
there verbatim).
- **The bar (`NavigationOverlay`, `@object-ui/components`).** The left
side is empty at the root: no object name, no "n / N". The title stays
as the dialog's accessible name only (Radix requires one), on every host
of the shared overlay. Three optional props draw the rest: `onBack` (a
`←` control; `Esc` calls it instead of closing), `trail` (the path after
`←`) and `stepper` (`↑` `↓`, each disabled at its end; `ArrowUp` / `k`
and `ArrowDown` / `j` on the document while the drawer is open, never
from a text field, a contenteditable region or a composite widget —
menu, listbox, grid, tree, tablist, … — and never with a modifier). `⤢`
stays icon-only, `✕` closes.
- **The root steps through the list.** `ListView`
(`@object-ui/plugin-list`) gains an `onRowsChange` host prop beside
`onTotalChange`: the rows it shows, in their order, re-reported when the
shown set changes after a load settles (page, sort, filter, search),
keyed on the ids in order so an equal refetch reports nothing. A paged
grid answers it; a board, a calendar or a grouped grid answers with no
rows, so a host never steps through a stale set. The object page threads
it to its drawer: `↑` `↓` at the root replace `?recordId=` (history
replace) by the previous / next row, disabled at the first and the last;
a list that reported no rows draws no `↑` `↓`.
- **The stack (`app-shell/src/views/recordDrawerStack.tsx`, new).**
`useRecordDrawerStack` holds the levels above a host-controlled root;
`RecordStackDrawer` draws the current level through the host's
`renderRecord` inside `RecordDrawerContext`. `←` / `Esc` pop one level,
`Esc` at the root closes, each ancestor in the path returns to its
level, `↑` `↓` replace the current level by its previous / next sibling
and never cross a level. Each level keeps its own `?tab=`: the left
level's tab is saved and the new level opens on its default tab; going
back restores it.
- **The door (`RelatedRecordActionsBridge`).** `openRecord` — which the
related list's `onView` and every lookup value (`ReferencedRecordLink` →
`host.openRecord`) already go through — opens the record in the nearest
drawer host when the target object opens in the drawer
(`navigation.mode` absent or `drawer`); an object that declares `page` /
`new_window` / `none` navigates as before, and with no host (Studio, a
custom page, the split pane, the other embedded hosts) nothing changes.
The decision reads a context, not the `embedded` flag: the record page
is a host too.
- **Siblings.** `RelatedList` hands its `onRowClick` the rows on show as
a third argument, and the `record:related_list` renderer turns them into
`RelatedRecordHandlers.onView`'s optional third argument (sibling ids).
Called with two arguments the renderer still calls `onView` with two.
- **The list host.** `ObjectViewRecordOverlay` draws `RecordStackDrawer`
over the root `?recordId=` names. `⤢` at the root keeps its
list-relative route and the list as origin; at a deeper level it opens
`/apps/:app/:object/record/:id?from=TRAIL` (TRAIL = the levels below
it), the existing `?from=` trail, so the page's back link reads the
record it was opened from and each ancestor's page carries the trail
above it. Either way the page receives the way back in history state.
- **The page host.** `RecordDetailView`, when not embedded, wraps its
page in `RecordPageDrawerHost`: a related row or lookup on the page
opens a drawer over it under the same rules (level 0 steps through the
page's related rows), the page's own `?tab=` is set aside while the
drawer is open, and `⤢` opens the record's page with the page record
heading the trail. The embedded view no longer draws the page's origin
back link inside a drawer.
- **The served approval-request page** is a drawer host like any record
page. Its two pins (`servedPageLookupEnter-12569`,
`servedPageRecordLink-12557`) now assert that the drawer opens over the
page with the target record in it, the router does not move, the href
stays the record path, and a modified click is the browser's.
- **`data-table` inside a dialog.** A table drawn inside a dialog opens
its rows. The click heuristic stopped every click with a dialog
ancestor, so related-list rows in the record drawer opened nothing. Now
only a dialog portalled out of the row (one that does not contain it)
stops the click (`data-table-rowClickInDialog-26679.test.tsx`).
- **「Collapse to drawer」.** A page opened by `⤢` shows "Collapse to
drawer" beside its back link; it returns to the host with the stack it
left. The restored stack is taken out of the history entry on arrival,
so a reload or Back shows the root alone.
- **What a reload keeps**, documented in the module docblock and in
`ui/content/docs/guide/user-state-persistence.md`: the root
(`?recordId=`); the levels above it are memory.
- **i18n.** `detail.collapseToDrawer` in all ten packs; the bar's `Go
back` / `Previous record` / `Next record` reuse the existing
`detail.goBack` / `detail.previousRecord` / `detail.nextRecord`. In zh
the `⤢` label reads 整页打开 (`detail.openAsFullPage` and
`console.objectView.expandToPage`, kept byte-identical by
`objectView-value-language-3582`), the same spelling as #27031's
`console.objectView.openAsPage`.
## What is not here
- **The top bar crumb.** The card's target breadcrumb (「线索 / 全部线索 / Mira
Costa / 任务 / 跟进报价」) has no carrier on main: `AppHeader` draws no crumb
after the app pill on an object route, record pages included (its
comment: an object route "draws NO crumb after the pill"). The path
rides the existing `?from=` trail, read by the page's back link; no
crumb is added here.
- **The split face** (the reader's Drawer | Split preference from
#27031) draws no `↑` `↓`: it is not a stack host.
## Screenshots
HotCRM lead Mira Costa → task, this branch's console (evidence branch
`claude/issue-26679-evidence-20261028`, never merged): drawer root
https://github.com/objectstack-ai/objectstack/blob/8ef7f64917413235c23cf1030260423f76b2d382/docs/evidence/issue-26679/1-drawer-root-lead.png
· one level down
https://github.com/objectstack-ai/objectstack/blob/8ef7f64917413235c23cf1030260423f76b2d382/docs/evidence/issue-26679/2-drawer-one-level-down-task.png
· after `⤢`
https://github.com/objectstack-ai/objectstack/blob/8ef7f64917413235c23cf1030260423f76b2d382/docs/evidence/issue-26679/3-expanded-task-page.png.
Taken before the list-root `↑` `↓` landed; the seat's console run on the
final head covers that control.
## Acceptance notes
- Other drawers that embed `RecordDetailView` (`InterfaceListPage`,
`ObjectDataPage`, `ObjectView`'s split pane) are not drawer hosts yet:
inside them a related row still navigates. Adopting `RecordStackDrawer`
there is the same swap `ObjectViewRecordOverlay` makes.
- The split pane still draws its object-label heading (reader preference
face, #26681's surface).
- `ui/content/docs/guide/user-state-persistence.md`'s "Authoring note"
still lists `modal` / `split` / `popover` as `navigation.mode` values,
retired by #26804; not edited here.
- No ObjectView-level integration pin wires `ListView` into a mounted
`ObjectView`; the producer (`ListView.onRowsChange-26679`) and the
consumer (`recordDrawerStack.rootStepper-26679`) are pinned separately
and the hook glue between them is about 25 lines.
## Tests
Final head `811a10fa14` (branch merged with `origin/main` `da8e6e719f`,
which includes #27031), local, macOS:
- Libraries built (`pnpm --filter '@object-ui/console^...' build`), then
`tsc --noEmit` plus `tsconfig.test.json` for plugin-list and app-shell:
exit 0.
- components + plugin-detail + i18n + react + plugin-list + the two
console approval pins: 1148 files passed, 2 skipped; 11809 tests.
- app-shell full (ObjectView tests included): 1817 passed, 1 failed
under load (`ListView.gridRowColorSelect-12393`, a loading-state
assertion); that file re-run alone passes 2/2.
- New in this PR: `navigation-overlay-bar-26679` (components, 9),
`recordDrawerStack-26679` (app-shell, 12),
`recordDrawerStack.rootStepper-26679` (app-shell, 4),
`ListView.onRowsChange-26679` (plugin-list, 2),
`RelatedList.rowClickSiblings-26679` (plugin-detail, 2),
`data-table-rowClickInDialog-26679` (components, 3), one sibling-ids
case in `RecordRelatedListRenderer.hostactions.test.tsx`; the two
console pins re-pinned as above.
- Reverse verification, each with the fix committed and a `grep -c`
count proving the mutation landed: the `ListView` emit line replaced →
both `onRowsChange` cases red; root stepping disabled in the stack → the
two presence / end cases red while the two absence controls stay green;
the old `closest('[role="dialog"]')` line restored → "a table inside a
dialog opens the clicked row" red while its two controls stay green. All
restored by `git checkout HEAD`, `git diff HEAD` 0 lines.
Verification ran in the lock's DECLARED UNLOCKED MODE: os-verify-lock:
VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was
serialized (macOS, no util-linux).
## Gates
Derived by `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `811a10fa14` (34 families), each run with
its exit code captured before any pipe; `--ran` reconciliation: 34
derived, 34 run, 0 UNRUN.
- 33 families exit 0.
- `pnpm --dir ui exec vitest run scripts/`: exit 1 with 6 files — 5
`Test timed out in 15000ms` under load (4 of them pass re-run alone: 44
tests) plus `select-affected-tests-wiring` (`mapfile: command not
found`, macOS bash 3.2, a host fact); `select-affected-tests-wiring` and
the `check-i18n-dead-keys` real-tree case stay NOT MEASURED locally;
CI's `Test UI` / `Lint & Repo Gates` read them.
- `pnpm check:new-line-citations` (ui/AGENTS.md #11): 0 new citations.
---
_Generated by [Claude
Code](https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff)_
---------
Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>1 parent a98daf7 commit 34c2407
37 files changed
Lines changed: 2015 additions & 107 deletions
File tree
- .changeset
- ui
- apps/console/src/services
- content/docs/guide
- packages
- app-shell/src/views
- __tests__
- components/src
- __tests__
- custom
- renderers/complex
- __tests__
- i18n/src/locales
- plugin-detail/src
- __tests__
- renderers
- plugin-list/src
- __tests__
- react/src
- context
- hooks/__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 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
26 | 31 | | |
27 | 32 | | |
28 | 33 | | |
| |||
207 | 212 | | |
208 | 213 | | |
209 | 214 | | |
210 | | - | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
211 | 218 | | |
212 | 219 | | |
213 | 220 | | |
| |||
310 | 317 | | |
311 | 318 | | |
312 | 319 | | |
313 | | - | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
314 | 327 | | |
315 | 328 | | |
316 | 329 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
28 | 34 | | |
29 | 35 | | |
30 | | - | |
| 36 | + | |
31 | 37 | | |
32 | 38 | | |
33 | 39 | | |
| |||
284 | 290 | | |
285 | 291 | | |
286 | 292 | | |
287 | | - | |
| 293 | + | |
288 | 294 | | |
289 | 295 | | |
290 | 296 | | |
291 | 297 | | |
| 298 | + | |
292 | 299 | | |
293 | 300 | | |
294 | 301 | | |
295 | | - | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
296 | 323 | | |
297 | 324 | | |
298 | 325 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
323 | 323 | | |
324 | 324 | | |
325 | 325 | | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
721 | 721 | | |
722 | 722 | | |
723 | 723 | | |
724 | | - | |
| 724 | + | |
725 | 725 | | |
726 | 726 | | |
727 | 727 | | |
| |||
815 | 815 | | |
816 | 816 | | |
817 | 817 | | |
| 818 | + | |
818 | 819 | | |
819 | 820 | | |
820 | 821 | | |
| |||
1165 | 1166 | | |
1166 | 1167 | | |
1167 | 1168 | | |
1168 | | - | |
1169 | 1169 | | |
1170 | 1170 | | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
1171 | 1174 | | |
1172 | 1175 | | |
1173 | 1176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
3758 | 3759 | | |
3759 | 3760 | | |
3760 | 3761 | | |
3761 | | - | |
| 3762 | + | |
3762 | 3763 | | |
3763 | 3764 | | |
3764 | 3765 | | |
| |||
3888 | 3889 | | |
3889 | 3890 | | |
3890 | 3891 | | |
3891 | | - | |
3892 | | - | |
3893 | | - | |
3894 | | - | |
3895 | | - | |
3896 | | - | |
3897 | | - | |
3898 | | - | |
| 3892 | + | |
| 3893 | + | |
| 3894 | + | |
| 3895 | + | |
| 3896 | + | |
| 3897 | + | |
| 3898 | + | |
| 3899 | + | |
| 3900 | + | |
| 3901 | + | |
| 3902 | + | |
| 3903 | + | |
| 3904 | + | |
| 3905 | + | |
| 3906 | + | |
| 3907 | + | |
| 3908 | + | |
| 3909 | + | |
| 3910 | + | |
3899 | 3911 | | |
3900 | 3912 | | |
3901 | 3913 | | |
| |||
4139 | 4151 | | |
4140 | 4152 | | |
4141 | 4153 | | |
| 4154 | + | |
| 4155 | + | |
| 4156 | + | |
| 4157 | + | |
| 4158 | + | |
| 4159 | + | |
| 4160 | + | |
| 4161 | + | |
| 4162 | + | |
| 4163 | + | |
| 4164 | + | |
| 4165 | + | |
| 4166 | + | |
| 4167 | + | |
| 4168 | + | |
| 4169 | + | |
| 4170 | + | |
| 4171 | + | |
| 4172 | + | |
| 4173 | + | |
| 4174 | + | |
| 4175 | + | |
| 4176 | + | |
| 4177 | + | |
| 4178 | + | |
4142 | 4179 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
19 | 24 | | |
20 | 25 | | |
21 | 26 | | |
| |||
91 | 96 | | |
92 | 97 | | |
93 | 98 | | |
| 99 | + | |
94 | 100 | | |
95 | 101 | | |
96 | 102 | | |
| |||
383 | 389 | | |
384 | 390 | | |
385 | 391 | | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
386 | 395 | | |
387 | | - | |
388 | | - | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
389 | 403 | | |
390 | 404 | | |
391 | | - | |
| 405 | + | |
392 | 406 | | |
393 | | - | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
394 | 416 | | |
395 | | - | |
| 417 | + | |
396 | 418 | | |
397 | 419 | | |
398 | 420 | | |
| |||
550 | 572 | | |
551 | 573 | | |
552 | 574 | | |
553 | | - | |
| 575 | + | |
554 | 576 | | |
555 | 577 | | |
556 | 578 | | |
| |||
653 | 675 | | |
654 | 676 | | |
655 | 677 | | |
656 | | - | |
| 678 | + | |
657 | 679 | | |
658 | 680 | | |
659 | 681 | | |
| |||
0 commit comments