Repository navigation
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
Conversation
…26679) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
… (objectstack#26679) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…6679) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…show parameter (objectstack#26679) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…awer-in-place-navigation
席位核对 · PR #27038 ← #26679 · epic PM
|
…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>
…tack#26679) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…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>
席位核对 · PR #27038 ← #26679 · epic PM
|
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).
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;Esccalls it instead of closing),trail(the path after←) andstepper(↑↓, each disabled at its end;ArrowUp/kandArrowDown/jon 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.ListView(@object-ui/plugin-list) gains anonRowsChangehost prop besideonTotalChange: 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↑↓.app-shell/src/views/recordDrawerStack.tsx, new).useRecordDrawerStackholds the levels above a host-controlled root;RecordStackDrawerdraws the current level through the host'srenderRecordinsideRecordDrawerContext.←/Escpop one level,Escat 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.RelatedRecordActionsBridge).openRecord— which the related list'sonViewand 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.modeabsent ordrawer); an object that declarespage/new_window/nonenavigates 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 theembeddedflag: the record page is a host too.RelatedListhands itsonRowClickthe rows on show as a third argument, and therecord:related_listrenderer turns them intoRelatedRecordHandlers.onView's optional third argument (sibling ids). Called with two arguments the renderer still callsonViewwith two.ObjectViewRecordOverlaydrawsRecordStackDrawerover 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.RecordDetailView, when not embedded, wraps its page inRecordPageDrawerHost: 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.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-tableinside 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).⤢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.ui/content/docs/guide/user-state-persistence.md: the root (?recordId=); the levels above it are memory.detail.collapseToDrawerin all ten packs; the bar'sGo back/Previous record/Next recordreuse the existingdetail.goBack/detail.previousRecord/detail.nextRecord. In zh the⤢label reads 整页打开 (detail.openAsFullPageandconsole.objectView.expandToPage, kept byte-identical byobjectView-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'sconsole.objectView.openAsPage.What is not here
AppHeaderdraws 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.↑↓: 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
RecordDetailView(InterfaceListPage,ObjectDataPage,ObjectView's split pane) are not drawer hosts yet: inside them a related row still navigates. AdoptingRecordStackDrawerthere is the same swapObjectViewRecordOverlaymakes.pageshows the declaration instead; the name cell's hover ↗ opens the record as a full page #26681's surface).ui/content/docs/guide/user-state-persistence.md's "Authoring note" still listsmodal/split/popoverasnavigation.modevalues, retired by feat(spec,ui)!: a record opens one way per object — navigation.mode keeps drawer (the default), page, new_window and none; modal, split and popover are retired; the split pane becomes a reader preference #26804; not edited here.ListViewinto a mountedObjectView; 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 withorigin/mainda8e6e719f, which includes #27031), local, macOS:pnpm --filter '@object-ui/console^...' build), thentsc --noEmitplustsconfig.test.jsonfor plugin-list and app-shell: exit 0.ListView.gridRowColorSelect-12393, a loading-state assertion); that file re-run alone passes 2/2.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 inRecordRelatedListRenderer.hostactions.test.tsx; the two console pins re-pinned as above.grep -ccount proving the mutation landed: theListViewemit line replaced → bothonRowsChangecases red; root stepping disabled in the stack → the two presence / end cases red while the two absence controls stay green; the oldclosest('[role="dialog"]')line restored → "a table inside a dialog opens the clicked row" red while its two controls stay green. All restored bygit checkout HEAD,git diff HEAD0 lines.Verification ran in the lock's DECLARED UNLOCKED MODE: os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable
flockon 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/objectstackon811a10fa14(34 families), each run with its exit code captured before any pipe;--ranreconciliation: 34 derived, 34 run, 0 UNRUN.pnpm --dir ui exec vitest run scripts/: exit 1 with 6 files — 5Test timed out in 15000msunder load (4 of them pass re-run alone: 44 tests) plusselect-affected-tests-wiring(mapfile: command not found, macOS bash 3.2, a host fact);select-affected-tests-wiringand thecheck-i18n-dead-keysreal-tree case stay NOT MEASURED locally; CI'sTest UI/Lint & Repo Gatesread 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