Repository navigation
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
Merged
objectstack-fleet[bot] merged 4 commits intoOct 28, 2026
Conversation
…tion writes the reader's drawer | split preference; the name cell's hover arrow opens the record as the full page (WIP) Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…rence and the name cell's open-as-page arrow Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…beside the row's record link, through a recordLinkAdornment host channel The title column rowAdornment picks is not where the record opens from: on HotCRM contacts it is the avatar column. The arrow now goes after the first authored link (or the auto-linked primary field) in the link cell itself. Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
…splay-record-open-preference
Contributor
Author
席位核对 · PR #27031 ← #26681 · epic PM
|
This was referenced Oct 28, 2026
objectstack-fleet
Bot
deleted the
claude/issue-26681-display-record-open-preference
branch
October 28, 2026 04:31
This was referenced Oct 28, 2026
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #26681
Clause-②: no
Metadata-protocol: none
Part of the record-page-v2 epic #26676 (child 4 of 4). Implements the maintainer's ruling 1 of 2026-10-27, quoted on the card: 「……split / popover 变成用户个人偏好……」 — the side-by-side pane is each reader's own choice, not metadata.
What changes
ViewSettingsPopover, after Layout, before Group). For a row that resolves todrawer: two segments 抽屉 | 分屏 writing the reader's preference (per browser, across objects,os:record-open-mode), and the note 「个人偏好,不进对象声明;两种都能展开为整页」. For an object or view that resolves topage: the read-only line 「此对象声明为整页打开」, no segments. Fornew_window/none: no section (see Acceptance notes). A host that passes norecordOpen(every host but the console) draws no section.app-shell/src/utils/recordOpenPreference.ts): the write sidewriteRecordOpenPreference(its storage type isStoragenarrowed tosetItem; no storage or a refused write throws nothing and the choice still holds on the page) anduseRecordOpenPreference, auseSyncExternalStorepair that hears this page's writes and another tab'sstorageevent.ObjectViewreplaces its read-onceuseState(readRecordOpenPreference)with it, so choosing 分屏 makes the next row click open the split face with no reload or remount.app-shell/src/views/objectViewOpenAsPage.tsx): beside the row's record link, hover-revealed by opacity (theRowActionMenuclass set, so it keeps its Tab stop; a coarse pointer always shows it),aria-labelandtitle「整页打开」 (console.objectView.openAsPage). It is a real anchor at the host'srecordHref; a plain click goes through the host'sopenRecord— the record route with the list as origin, the same destination as the drawer's own expand — and stops before the row's click, so neither the drawer nor the pane opens and the preference is untouched. Drawn only for a drawer-mode row: apagerow already opens the page, andnoneopens nothing.recordLinkAdornment(ObjectGrid→LinkCell, relayed byListView): a host node after the record link, never inside it, beside the first column the view links (link: true), else beside the auto-linked primary field. Absent, the link cell is byte-identical to before (theObjectGrid.linkCellAnchormarkup pin stays green).list.*keys andconsole.objectView.openAsPagein all ten packs, plus theListViewdefaults-map rows thedefaults-maps-mirror-en-packtest holds to the en pack.No metadata is added and
navigationreading is unchanged;packages/spec/**is not touched, nor any file of #26679 (navigation-overlay.tsx,objectViewRecordNavigation.ts,RelatedRecordActionsBridge.tsx,RecordDetailView.tsx).Route taken against the dispatch's mechanism hypotheses
PluginObjectView.ObjectView'srenderListViewcallback (objectViewListRenderer.tsx) rendersListViewitself, sorecordOpengoes ObjectView →renderListView→ListView→ViewSettingsPopover, one hop shorter;PluginObjectViewis unchanged. Both new props are React host props (function values / host state), never node keys: nothing entersGRID_QUERY_INPUTSor the SDUI manifest.rowAdornmentchannel and was moved off it. Measured on HotCRM contacts:rowAdornmentsits beside the row's TITLE (the declared name field's column when shown, else the first shown column), and contact'snameField: 'full_name'is not a column, so the arrow landed in the avatar column, away from 「名」 where the row opens. Hence the newrecordLinkAdornmentchannel at the link cell (screenshot below).Screenshots (HotCRM
1f3f8880on a fresh DB, console served from this branch'sui/src)Evidence lives on a never-merged branch, commit
964025cf3f3f7a2ecfd4a743d250699f2ec552ec.Leads (the lead view declares
navigation.mode: 'page') — the read-only line:Opportunities (declares
drawer) — the two segments and the note:Contacts, first row hovered — the ↗ beside 「John」, the record link:
Tests
Measured on head
3d7fb4c462(branch tip after mergingorigin/main), fromui/:pnpm exec vitest run --maxWorkers=3 packages/app-shell/ packages/plugin-grid/ packages/plugin-list/ packages/i18n/—Test Files 2307 passed | 1 skipped (2308),Tests 21776 passed | 22 skipped (21798), exit 0.plugin-list/src/__tests__/ViewSettingsPopover.recordOpen-26681.test.tsx(drawer segments and note, Split writessplit, the pressed segment writes nothing,pageread-only line, no section fornew_window/none/ no prop, zh pack);plugin-grid/src/__tests__/ObjectGrid.recordLinkAdornment-26681.test.tsx(node beside the authored link as the anchor's sibling, one per row, none on the auto-linked first column, absent prop leaves no wrapper, click never reaches the row);app-shell/src/views/ObjectView.recordOpenDisplay-26681.test.tsx(choosing 分屏 keepssplitand the next row click draws the split face; no choice opens the drawer; apageview shows the read-only line and no ↗; the ↗ is an anchor at the record route beside the link, and a click lands on the record route with no drawer and no preference written);app-shell/src/utils/recordOpenPreference.test.tsgains the write side.pnpm --filter @object-ui/i18n --filter @object-ui/plugin-grid --filter @object-ui/plugin-list --filter @object-ui/app-shell run type-check(after building the three libraries'dist) — exit 0, eachtype-checkscript echoed.ListViewProps['recordOpen'], read by app-shell from plugin-list's rebuilt.d.ts), viascripts/ablation-replace.mjswith a trap restore, committed state first: replacingresolvedMode: navOverlay.modewithresolvedMode: 'split' as constturns app-shell'stsc --noEmitred —TS2322: Type '"split"' is not assignable to type '"none" | "drawer" | "page" | "new_window"', exit 2; restored, blob back to43072da53b2a,git diff HEADempty. A first attempt that added an extra key instead stayed green (exit 0): a contextually typed arrow's return gets no excess-property check, so that leg proved nothing and was replaced, not counted.Acceptance notes
new_window/nonedraw no Open records section: the preference changes nothing there and the ruling gives those declarations no line of their own. A reader of such an object sees no setting rather than a control that does nothing.compactToolbar: false) has no twin. No host that passesrecordOpendraws the wide form — the console never relays a view'scompactToolbartoListView— so the popover's "change one, change both" note now names this exception.console.objectView.expandToPage「以完整页面打开」 for the same English sentence. Two zh spellings of one action on one page — noted, not changed here (the drawer's controls belong to console record drawer v2: the bar is ↑ ↓ · ⤢ · ✕ (no object name, no counter); a related row or a lookup opens its record in place with a trail, ← / Esc go back one level; ⤢ carries the trail into the page; on the record page a related row opens a drawer over the page #26679).recordLinkAdornmentreachesObjectGrid); kanban, gallery and the other visualizations open records from cards with no name cell.Verification
Gate families from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(derived from the merge base withorigin/main, at3d7fb4c462; 35 commands), each run on its own with the exit code captured before any pipe:node scripts/check-adr-0087-registration.mjs --base origin/main— exit 0node scripts/check-adr-0087-registration.mjs --self-test— exit 0node scripts/check-changeset-no-major.mjs --base origin/main— exit 0node scripts/check-closing-keyword-parity.mjs— exit 0node scripts/check-closing-keyword-parity.mjs --self-test— exit 0node scripts/check-comment-mask-corpus.mjs— exit 0node scripts/check-empty-changeset.mjs --base origin/main— exit 0node scripts/check-empty-changeset.mjs --self-test— exit 0node scripts/check-sdui-manifest.mjs— exit 0node scripts/check-sdui-manifest.mjs --self-test— exit 0node scripts/pm/release-rehearsal-clone.mjs --self-test— exit 0node scripts/release-pending-publish.mjs --self-test— exit 0pnpm --dir ui exec vitest run scripts/— exit 1pnpm --filter @objectstack/spec run check:duration-unit-keys— exit 0pnpm check:changeset-gate-self-tests— exit 0pnpm check:closed-ceremony-text— exit 0pnpm check:cross-package-test-inputs— exit 0pnpm check:dispatcher-error-vocabulary— exit 0pnpm check:driver-memory-census— exit 0pnpm check:engine-double-contract— exit 0pnpm check:error-status-conformance— exit 0pnpm check:gitlink-declared— exit 0pnpm check:nul-bytes— exit 0pnpm check:pm-changeset-deadline-census— exit 0pnpm check:published-files— exit 0pnpm check:query-options-erasure— exit 0pnpm check:refd-timer-probe— exit 0pnpm check:skill-identifier-liveness— exit 0pnpm check:test-source-alias— exit 0pnpm check:tier-file-adoption— exit 0pnpm check:type-check-coverage— exit 0pnpm check:type-check-debt— exit 0pnpm check:undeclared-dep-imports— exit 0pnpm check:watch-hint-literal— exit 0pnpm check:where-matcher— exit 0The one red is the
ui/scriptsvitest battery, which ran beside the four-package suite on a box at load average 14: 14 files, 22 tests.select-affected-tests-wiring.test.tsfails on this host by construction (shard.sh: line 2: mapfile: command not found, exit 127 — macOS's bash 3 has nomapfile; CI runs Linux). The other failures are tests timing out at roughly 20 s each; none of their files reads a path this diff changes. They are rerunning in isolation now; that verdict goes in theos-dev-reportcomment on #26681, and CI's own run is the authority.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the commands above were run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for these
runs, nor for any sibling agent in this container while they ran.
test:repo(the root vitestrepoproject) is CI's, not run here: the diff touches no file of that project.Generated by Claude Code