Skip to content

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

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-26679-drawer-in-place-navigation
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-26679-drawer-in-place-navigation

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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 feat(app-shell,plugin-list,plugin-grid,i18n): the Display menu's Open records section writes the reader's drawer | split preference; the name cell's ↗ opens the record as the full page #27031's console.objectView.openAsPage.

What is not here

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

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 Migrate documentation site to Fumadocs with monorepo structure and shared content #11): 0 new citations.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #27038 ← #26679 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-28T02:43Z

Verdict: REWORK (patch round on this PR, same claim) — not governed (0 of 28 paths), +1602 / −94. The drawer side is judged complete on the branch; the card's list-root half is held only by the plugin-list fence, which lifts when #27031 (#26681) merges — it is queued now. One PR lands the whole card rather than a Part of plus a follow-up.

What the seat read and accepts as built (head ec88340391):

  • navigation-overlay.tsx: three optional props onBack / trail / stepper; the title goes sr-only on every host (ruling 2); Escape pops when onBack is set; ArrowUp / k, ArrowDown / j on the document while the drawer is open, never inside a text field, a composite widget or with modifiers (ruling 3's ↑ ↓, no counter).
  • app-shell/src/views/recordDrawerStack.tsx + recordDrawerContext.ts: an in-memory stack over a host-controlled root, ← plus a clickable path, per-level ?tab= kept and restored, ↑ ↓ through the level's sibling set (the related list's rows on show, threaded RelatedList.onRowClick → onView third argument), ⤢ at any level opens the record page with the levels below as its existing ?from= trail, and the page's 「收为抽屉」 link restores the stack once from history state.
  • RelatedRecordActionsBridge.openRecord: related rows and lookups share one handler; with a drawer host and a drawer-mode target it pushes in place, a page / new_window / none object still navigates (ruling 5, never a second drawer); RecordDetailView is a host too (RecordPageDrawerHost).
  • Tests: app-shell 1801 files / 16511 passed; components + plugin-detail + i18n + react 977 files with the one arity pin re-pinned at a per-site arity of 3; new suites navigation-overlay-bar-26679 (9), recordDrawerStack-26679 (12), RelatedList.rowClickSiblings-26679 (2). Gates 34 / 34 run, the vitest run scripts/ red is the known macOS host gap. CI in progress at the time of writing.
  • Premise corrections accepted: the top bar draws no crumb on object routes, so the path rides ?from= and the page back link (the card's crumb line was aspirational); the dispatch wrongly said the ⤢ tooltip was done.

Patch round (one stroke, after #27031 is on main):

  1. Merge origin/main (brings ListView's recordOpen / recordLinkAdornment host props from feat(app-shell,plugin-list,plugin-grid,i18n): the Display menu's Open records section writes the reader's drawer | split preference; the name cell's ↗ opens the record as the full page #27031); add onRowsChange to ListView (sibling of onTotalChange, the rows on show in their shown order), and have ObjectView pass root.siblings + onRootStep to useRecordDrawerStack so the list root draws ↑ ↓ like the page host does — then change the body's first line to Fixes #26679.
  2. zh copy: the drawer's expand control and the list's ↗ are one action — set console.objectView.expandToPage (zh) to 「整页打开」 so it matches console.objectView.openAsPage from feat(app-shell,plugin-list,plugin-grid,i18n): the Display menu's Open records section writes the reader's drawer | split preference; the name cell's ↗ opens the record as the full page #27031; en stays.
  3. Screenshots (card acceptance 2): HotCRM lead → task at the drawer root, one level down, and the page opened by ⤢ from the task level, on a never-merged evidence branch, linked in the body — or state that the seat's own preview run stands in, and the seat will run it on the final head.
  4. Keep the ui doc drift note (user-state-persistence.md listing retired modes) in Acceptance notes; not this card.

Generated by Claude Code

hotlong and others added 3 commits October 28, 2026 11:38
…ge's record drawer; zh expand label reads 整页打开 (objectstack#26679)

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…nly a dialog portalled out of the row stops the click (objectstack#26679)

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
hotlong and others added 3 commits October 28, 2026 12:38
…awer-in-place-navigation

# Conflicts:
#	ui/packages/app-shell/src/views/ObjectView.tsx
#	ui/packages/i18n/src/locales/zh.ts
…he rows the list shows (objectstack#26679)

ListView reports the rows on show (onRowsChange, beside onTotalChange); the
object page hands them to its drawer as the root's sibling set and steps the
root through ?recordId=.

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…n-list joins (objectstack#26679)

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #27038 ← #26679 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-28T06:33Z

Verdict: ACCEPT — ordinary queue landing (governed register: 0 of 37 paths; +2015 / −107, under the 3000-line threshold). Supersedes the REWORK above: both patch rounds landed on this PR under the same claim.

Checklist, judged on GitHub and on the branch:

Landing: ready + auto-merge in this stroke on the maintainer's standing authorization for this epic (verbatim, 2026-10-28: 「授权你用车队身份给 #26758 翻 ready 并挂 auto-merge(squash),后续本 epic 的 PR 复核 ACCEPT 后同样处理」); the queue decides the rest.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 06:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 06:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 34c2407 Oct 28, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-26679-drawer-in-place-navigation branch October 28, 2026 06:57
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/xl tests tooling

Projects

None yet

2 participants