Skip to content

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 into
mainfrom
claude/issue-26681-display-record-open-preference
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-26681-display-record-open-preference

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

  • Display menu, 「打开记录」 section (ViewSettingsPopover, after Layout, before Group). For a row that resolves to drawer: 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 to page: the read-only line 「此对象声明为整页打开」, no segments. For new_window / none: no section (see Acceptance notes). A host that passes no recordOpen (every host but the console) draws no section.
  • The preference is live (app-shell/src/utils/recordOpenPreference.ts): the write side writeRecordOpenPreference (its storage type is Storage narrowed to setItem; no storage or a refused write throws nothing and the choice still holds on the page) and useRecordOpenPreference, a useSyncExternalStore pair that hears this page's writes and another tab's storage event. ObjectView replaces its read-once useState(readRecordOpenPreference) with it, so choosing 分屏 makes the next row click open the split face with no reload or remount.
  • The name cell's ↗ (app-shell/src/views/objectViewOpenAsPage.tsx): beside the row's record link, hover-revealed by opacity (the RowActionMenu class set, so it keeps its Tab stop; a coarse pointer always shows it), aria-label and title 「整页打开」 (console.objectView.openAsPage). It is a real anchor at the host's recordHref; a plain click goes through the host's openRecord — 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: a page row already opens the page, and none opens nothing.
  • Grid host channel recordLinkAdornment (ObjectGrid → LinkCell, relayed by ListView): 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 (the ObjectGrid.linkCellAnchor markup pin stays green).
  • i18n: five list.* keys and console.objectView.openAsPage in all ten packs, plus the ListView defaults-map rows the defaults-maps-mirror-en-pack test holds to the en pack.

No metadata is added and navigation reading 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

  • The props do not travel through plugin-view's PluginObjectView. ObjectView's renderListView callback (objectViewListRenderer.tsx) renders ListView itself, so recordOpen goes ObjectView → renderListView → ListView → ViewSettingsPopover, one hop shorter; PluginObjectView is unchanged. Both new props are React host props (function values / host state), never node keys: nothing enters GRID_QUERY_INPUTS or the SDUI manifest.
  • The ↗ first went through the grid's existing rowAdornment channel and was moved off it. Measured on HotCRM contacts: rowAdornment sits beside the row's TITLE (the declared name field's column when shown, else the first shown column), and contact's nameField: 'full_name' is not a column, so the arrow landed in the avatar column, away from 「名」 where the row opens. Hence the new recordLinkAdornment channel at the link cell (screenshot below).

Screenshots (HotCRM 1f3f8880 on a fresh DB, console served from this branch's ui/ src)

Evidence lives on a never-merged branch, commit 964025cf3f3f7a2ecfd4a743d250699f2ec552ec.

Leads (the lead view declares navigation.mode: 'page') — the read-only line:

leads Display menu

Opportunities (declares drawer) — the two segments and the note:

opportunities Display menu

Contacts, first row hovered — the ↗ beside 「John」, the record link:

contacts row hover

Tests

Measured on head 3d7fb4c462 (branch tip after merging origin/main), from ui/:

  • 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.
  • New suites: plugin-list/src/__tests__/ViewSettingsPopover.recordOpen-26681.test.tsx (drawer segments and note, Split writes split, the pressed segment writes nothing, page read-only line, no section for new_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 分屏 keeps split and the next row click draws the split face; no choice opens the drawer; a page view 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.ts gains 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, each type-check script echoed.
  • Reverse verification of the cross-package type (ListViewProps['recordOpen'], read by app-shell from plugin-list's rebuilt .d.ts), via scripts/ablation-replace.mjs with a trap restore, committed state first: replacing resolvedMode: navOverlay.mode with resolvedMode: 'split' as const turns app-shell's tsc --noEmit red — TS2322: Type '"split"' is not assignable to type '"none" | "drawer" | "page" | "new_window"', exit 2; restored, blob back to 43072da53b2a, git diff HEAD empty. 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

Verification

Gate families from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (derived from the merge base with origin/main, at 3d7fb4c462; 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 0
  • node scripts/check-adr-0087-registration.mjs --self-test — exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main — exit 0
  • node scripts/check-closing-keyword-parity.mjs — exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test — exit 0
  • node scripts/check-comment-mask-corpus.mjs — exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main — exit 0
  • node scripts/check-empty-changeset.mjs --self-test — exit 0
  • node scripts/check-sdui-manifest.mjs — exit 0
  • node scripts/check-sdui-manifest.mjs --self-test — exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test — exit 0
  • node scripts/release-pending-publish.mjs --self-test — exit 0
  • pnpm --dir ui exec vitest run scripts/ — exit 1
  • pnpm --filter @objectstack/spec run check:duration-unit-keys — exit 0
  • pnpm check:changeset-gate-self-tests — exit 0
  • pnpm check:closed-ceremony-text — exit 0
  • pnpm check:cross-package-test-inputs — exit 0
  • pnpm check:dispatcher-error-vocabulary — exit 0
  • pnpm check:driver-memory-census — exit 0
  • pnpm check:engine-double-contract — exit 0
  • pnpm check:error-status-conformance — exit 0
  • pnpm check:gitlink-declared — exit 0
  • pnpm check:nul-bytes — exit 0
  • pnpm check:pm-changeset-deadline-census — exit 0
  • pnpm check:published-files — exit 0
  • pnpm check:query-options-erasure — exit 0
  • pnpm check:refd-timer-probe — exit 0
  • pnpm check:skill-identifier-liveness — exit 0
  • pnpm check:test-source-alias — exit 0
  • pnpm check:tier-file-adoption — exit 0
  • pnpm check:type-check-coverage — exit 0
  • pnpm check:type-check-debt — exit 0
  • pnpm check:undeclared-dep-imports — exit 0
  • pnpm check:watch-hint-literal — exit 0
  • pnpm check:where-matcher — exit 0

The one red is the ui/scripts vitest battery, which ran beside the four-package suite on a box at load average 14: 14 files, 22 tests. select-affected-tests-wiring.test.ts fails on this host by construction (shard.sh: line 2: mapfile: command not found, exit 127 — macOS's bash 3 has no mapfile; 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 the os-dev-report comment on #26681, and CI's own run is the authority.

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not 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 vitest repo project) is CI's, not run here: the diff touches no file of that project.


Generated by Claude Code

hotlong and others added 4 commits October 28, 2026 08:13
…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #27031 ← #26681 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-28T02:35Z

Verdict: ACCEPT — ordinary queue landing (governed register: 0 of 24 paths; +999 / −18, under the 3000-line threshold).

Checklist, judged on GitHub and on the branch:

Landing: ready + auto-merge in this stroke on the maintainer's standing authorization for this epic; 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 02:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 02:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 7414fc8 Oct 28, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-26681-display-record-open-preference branch October 28, 2026 04:31
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