Skip to content

feat(console): the list title switches views from a dropdown, a pinned view opens by default, and a starred view joins the sidebar favorites - #27943

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-vbf1ad
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-vbf1ad

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread

Fixes #27594

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:对象列表的标题变成视图切换下拉「视图名 ▾ · 条数」(搜索框常在,分「📌 固定 · 最近 · 全部」三段,全部里作者视图在前、用户保存的视图在后,底部「+ 新建视图」和管理员的「管理视图」);平铺视图页签行退役;📌 让某视图成为本人打开该对象时的默认视图(按对象按人存);☆ 把视图放进侧栏「收藏」组,可拖动排序,菜单里已有的视图 ☆ 置灰并提示「已在菜单中」。
  • 为什么改:每种元数据的视图数量不定,页签行五个就溢出,在 464px 的分屏列表里更挤(epic epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676 的卡)。
  • 风险与回滚:只动 ui(app-shell 私有包 + i18n 十个语言包各加 7 个键 + 文档一节);不改元数据、协议、spec;收藏视图存进现有收藏库(type view),固定视图存本浏览器的 workspacePreference;回滚即 revert 本 PR。
  • 先看:views/objectViewTitleSwitcher.tsx(下拉组件与组装它的 hook)、ObjectView.tsx(📌 默认视图的解析顺序:地址 > 固定 > 作者默认 > 第一个)、layout/listViewFavorites.ts 与 layout/UnifiedSidebar.tsx(☆ 与收藏组拖序)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/ObjectView.titleViewSwitcher-27594.test.tsx packages/app-shell/src/views/objectViewTitleSwitcher.test.tsx,预期两个文件全绿。
  • 需要你做的:无;epic 座位在控制台预览里验收(卡 Acceptance 3)。下方「Open question」有一处前提偏差(对象路由没有面包屑)请座位定夺。

What changes

Measured first, on main at ddab8b99: the object list page drew its views as a flat tab row in the list's toolbar (ViewTabBar, handed to ListView as toolbarLeading, objectui#12440), the page header's h1 read the object's plural, and a reader could not pin or star a view.

  • The title is the view switcher. ObjectViewTitleSwitcher (new, views/objectViewTitleSwitcher.tsx) is the page header's title: the open view's label, ▾, and · <count> once the list reports its total (ListView.onTotalChange, kept per view id). It opens a popover with a search box (always), then 📌 Pinned, Recent (at most three, the pinned one excluded) and All (the authored views in their order, then the views saved at runtime), each row with its view-type icon and, on hover or focus, ☆ and 📌. Its foot is + New view (the existing create-view flow) and, for an admin, Manage views (the existing dialog). A search lists the matching views flat, in All's order. The split pane's list (Record drawer v4: a bounded peek over the list — md cap instead of the field-count bucket, no dim overlay, the list stays readable and clickable, ⤢ maximizes in place at the same URL; the full page is reached only from the row ↗ and the header ⋯ #27545) is the same page, so it has the same switcher; nothing is special-cased.
  • The flat tab row retires. objectViewTabBar.tsx is deleted; the list gets no toolbarLeading, and a chart view no longer draws its own tab row above the chart. The filter / sort / display controls keep their own row.
  • 📌 Pinned = the reader's default. LIST_PINNED_VIEW_PREFERENCE (list.view.pinned, scope object:NAME) in console: one workspace-preference module (record open mode, drawer width, timeline / related column toggles) read per user × object, with a shared panel-toggle toolbar on the record page — defaults derived from the object, never metadata #27052's workspacePreference module. A bare list address opens the pinned view, else the authored default (isDefault), else the first; a view in the address still wins. A pin naming a view the list no longer offers is no pin. Unpinning stores null.
  • ☆ Starred views in the sidebar's Favorites (收藏). A star is a favorite of type view (id view:OBJECT:VIEW, href /apps/APP/OBJECT/view/VIEW, the object's icon) in the existing favorites store. The sidebar draws a view favorite only in the app its address names, beside the object's icon, and the section's own rows (views, records, objects, dashboards) can now be dragged into order, or moved with Space and the arrow keys, through reorderFavorites (SortableFavoriteRows, the same gesture as the pinned entries of objectui#12059). A view an app navigation entry already opens (an object entry with that viewName, as the member is shown it) shows its ☆ disabled with 「已在菜单中」 / "Already in the menu".
  • The view description is shown once. The tab row used to carry it as the active tab's tooltip. A list face now draws it through ListView's own info control at the start of its toolbar row (its behaviour for a host with no leading node); a chart face, which has no toolbar row, carries it as the switcher's tooltip.
  • i18n: view.switcher.{label,pinned,recent,all,pin,unpin,inMenu} in all ten packs. Docs: content/docs/ui/views.mdx gains "Switching views".

Deviations from the card, each agreed with the seat before the code

  • File surface. The card names ui/packages/plugin-list/src/** for the tab row → switcher. The console's tab row is drawn by app-shell (objectViewTabBar.tsx, objectViewPageHeader.tsx, objectViewViewCrud.tsx), so the switcher lives in app-shell (objectViewTitleSwitcher.tsx, new) and plugin-list is untouched. The card's components/UnifiedSidebar.tsx is layout/UnifiedSidebar.tsx.
  • Where a star is stored (seat ruling B). Target 4 says the 收藏 entry is a "reader preference, same module, scope app:NAME". It is stored in the existing favorites store instead (FavoritesProvider, the ui.favorites user-preference row, type view), filtered by the app its address names. Reason: the sidebar's 收藏 section already draws that store (objectui#12427), so a second store would split one section into two lists with two orders, and the favorites row follows the reader across devices where workspacePreference is this browser's.
  • Recent is read from the recent-items list the console already keeps for list-view visits (RecentViewItem, objectui#12604).
  • Manage views at the dropdown's foot (admins only) is an addition the card did not name: the tab row's ▾ menu was the only way into that dialog, and it retires with the row.

Tests

New:

  • views/objectViewTitleSwitcher.test.tsx (13): label and count; authored-then-saved order in All; search box always there; Pinned → Recent (≤3, without the pinned) → All; a pinned view the list no longer offers draws no Pinned; case-insensitive search narrows flat; choosing switches and closes; 📌 pin / unpin; ☆ toggles, and a menu view's ☆ is disabled with its reason; no ☆ off an app route; the foot and its CONTROL.
  • views/ObjectView.titleViewSwitcher-27594.test.tsx (real ObjectView): the title in the h1 with the view label and count; no tab bar, tab row or toolbar slot; the dropdown's order; search and its empty state; pin stores {"view":"v3","v":1} at os:pref:list.view.pinned@object:crm_lead and a bare route opens it on remount; unpin returns to the default; a view in the address beats the pin; the split pane (?recordId= with the split preference) shows the same switcher.
  • views/ObjectView.starredView-27594.test.tsx: ☆ writes the view favorite (id, label, href, icon); an authored viewName entry's ☆ is disabled with "Already in the menu".
  • layout/__tests__/UnifiedSidebar.starredViews-27594.test.tsx: a starred view of this app is drawn with its icon and address, one of another app is not; a keyboard reorder saves the new order.

Updated (they read the retired tab row, a tab's text, or the plural h1): ObjectView.chartViewTabs-13114, viewRefusal-12112, labelLessViewTab-12774, servedViewLabel-11295, userListState-12418, viewWriteRefusal-11583, ufViewSwitch-11992, createViewRefusal-11578, createKanbanView-11581, createChartView-11576, recordOpenPreference-26677, refreshInPlace-10035, externalRefreshSum-10572, relayRungCensus-7559, listTitlePlural-11696, managedByBadgeAuthoring-12194, setupListBadge-13385, listActionsReadGate-12109, objectBoundActions-7234, and console recordOpenFromEveryView-27057. ObjectView.viewTabsInToolbar-12440.test.tsx is replaced by ObjectView.titleViewSwitcher-27594.test.tsx (the toolbar slot it pinned is gone).

Acceptance notes

  • Open question (premise drift). The card says the object name "stays in the breadcrumb and the sidebar highlight". An object route draws no crumb (objectui#12461: the page header named the object, so a crumb said it twice), so with the title naming the view the object is named by the sidebar's active entry and the header's object icon. A label-less view is still called by the plural (objectui#12774), and so are the tests of docs(skills): regenerated responsive.zod.ts reference line for the #11027 retirement (governed surface — human merge, lands after #11685) #11696 and the badge tests, which now open such a view. Restoring an object crumb on list routes is a one-line call for the seat; this PR does not make it.
  • Screenshots are on examples/app-crm (HotCRM is not in this workspace), a deviation from Acceptance 2, and are kept in the project's files for the seat rather than on an evidence branch; see Verification. Two layout faults they showed were fixed before this PR opened: the trigger's negative margin cut a four-character label by one, and its outer focus ring was clipped by the title's truncation.
  • The count is the list's reported total, so it is drawn for a view whose list reports one (the paged grid); a board or calendar that does not report a total shows the label and ▾ alone.
  • The admin-shared isPinned flag of a saved view (the old tab row's pin) is not drawn in the dropdown; 📌 is the reader's own pin.
  • The phone keeps its app-bar view switcher (MobileViewSwitcherContext); the page header with the title switcher is sm and up, as before.
  • Seen while taking the split-pane screenshot, not changed here: in the split arm the list draws its own record-count bar and ObjectView draws record-count-footer under it, so 「5 条记录」 shows twice (ObjectView.tsx, the split pane's footer; untouched by this diff).
  • packages/spec/** is untouched. One spec description still names the tab strip: view.list.tabs' removal note says "the tab strip above an object's records is the saved-view switcher (ViewTabBar)". It is prose in the generated reference; noted for the spec lane, not changed here.

Verification

Local runs (head 16c2570, after merging origin/main 04740b7 once)

All under scripts/pm/os-verify-lock.sh:

  • pnpm --dir ui exec vitest run over the 238 test files that name ObjectView, the retired tab bar or toolbar slot, FavoritesProvider/useFavorites, workspacePreference, objectViewListRenderer, objectViewPageHeader, StarredApps or list-view-switcher (app-shell views, layout, context, utils, console home and AppContent tests, console recordOpenFromEveryView-27057), plus packages/i18n/ → Test Files 330 passed (330), Tests 3335 passed, 13 skipped; VITEST_EXIT=0.
  • pnpm --filter @object-ui/app-shell run type-check (both tsc programs) → TYPECHECK_EXIT=0.
  • eslint on every changed app-shell file → 0 errors (warnings carried over from the deleted objectViewTabBar.tsx).
  • pnpm --dir ui run check:i18n-drift → 0 en values changed, 7 keys added; check:i18n-keys and check:i18n-dead-keys → exit 0; pnpm check:new-line-citations (ui) → 0 new citations, exit 0.
  • Not run locally, declared to CI: the whole Test UI shards, the console type-check (no export of @object-ui/app-shell changed; one console test file changed), and the rest of the lint farm.
Ablations (each through scripts/ablation-replace.mjs, each restored to HEAD with an empty diff)
  • ObjectView.tsx: the pin dropped from the open-view resolution (resolvedViewId || defaultViewId) → titleViewSwitcher-27594 1 of 7 red (the pin is kept and the bare address opens it).
  • objectViewTitleSwitcher.tsx: authored-first order dropped → objectViewTitleSwitcher.test 1 of 13 red.
  • UnifiedSidebar.tsx: view favorites not filtered by app → UnifiedSidebar.starredViews-27594 2 of 2 red.
  • listViewFavorites.ts: inMenu always false → starredView-27594 1 of 2 red.
  • UnifiedSidebar.tsx: the sidebar's drop not saved (onReorder={() => {}}) → UnifiedSidebar.starredViews-27594 1 of 2 red.
Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 16c257005a derived 66 families (the same list as at 8aec374c05); each run and recorded as command :: exit code before any pipe; all 66 exit 0, including node scripts/check-sdui-manifest.mjs and pnpm --dir ui exec vitest run scripts/ (run alone, nothing else running). pnpm --filter @objectstack/spec run check:skill-examples exited 3 (NOT MEASURED: stale @objectstack/client-react declarations) on the first pass at 8aec374c05; after pnpm --filter @objectstack/client-react build it exited 0, and 0 again at 16c257005a. --ran reconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.

Screenshots (examples/app-crm, zh-CN, 1440 × 900)

Backend examples/app-crm with objectstack dev --seed-admin --fresh on its own port, Console from ui/apps/console (Vite, src) pointed at it, driven by Playwright; both stopped afterwards. Eight PNGs in the project's files for the seat (ui-seat2/card-27594/shots/):

  • 00-list-title.png: 「全部线索 ▾ · 5」 as the title; no tab row; the filter / sort / display controls on their own row.
  • 01-dropdown-open.png: the dropdown open, a row hovered with ☆ and 📌, 「新建视图」 and 「管理视图」 at the foot.
  • 02-bare-address-opens-pinned.png: after 📌 on 「线索看板」, the bare list address opens the board.
  • 03-dropdown-pinned-recent-all.png: 「固定 · 最近 · 全部」.
  • 04-dropdown-search.png: searching 「看板」.
  • 05-sidebar-starred-view.png: 「收藏」 at the top of the sidebar with the starred 「线索看板」 beside the object's icon.
  • 06-split-pane.png, 07-split-pane-dropdown.png: a record open in the split pane; the list beside it keeps the same switcher, and it opens there.

Generated by Claude Code

…tarred view joins the sidebar favorites

Claude-Session: https://claude.ai/code/session_01Utp6CanDVECEsojSwtvSs7
Co-authored-by: Claude <noreply@anthropic.com>
…tion shows once, and the docs and changeset describe the switcher

The title switcher carries a view's description only on a chart face; a list
face keeps the list's own info control at the start of its toolbar row, so the
text is shown once. The object-list tests that read the retired tab row, the
title's plural or a tab's text read the title switcher instead. The views page
documents switching, pinning and starring a view.

Claude-Session: https://claude.ai/code/session_01Utp6CanDVECEsojSwtvSs7
Co-authored-by: Claude <noreply@anthropic.com>
…ocus ring

The trigger's negative margin was taken out of the width the title is sized
to, so a four-character label was cut by one; its outer focus ring was
clipped by the title's own truncation. Both seen on the CRM example.

Claude-Session: https://claude.ai/code/session_01Utp6CanDVECEsojSwtvSs7
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Preview review (proxy for epic:#26676#1) · 16c2570 · PASS

Reviewed 2026-10-30T11:06Z under the #26676 ruling B (comment 6520722066). A context-free review subagent with no development context got three inputs: #27594's Target and Acceptance (with the three rulings that change how it is read: proxy preview on examples/app-crm, stars in the existing favorites store, no object breadcrumb → #27950), the v4 boards (git show a7825499aa:docs/design/record-page-v2-2026-10-27/v4/…), and this PR's "What changes". It was told not to read the source, the diff or the tests. It walked the PR head's live console in this container: examples/app-crm backend (objectstack dev --seed-admin --fresh), Console from ui/apps/console at head 16c257005a, admin, zh-CN, 1440 × 900, plus 1280 and the English pack.

Reason. All five Target items are met. The leads title reads 「全部线索 ▾ · 5」 and no flat tab row is drawn on the table or the kanban face; filter / sort / display keep their own row. The dropdown has the always-on search, 固定 · 最近 · 全部, authored-then-saved order, view-type icons, ☆ / 📌 on hover, and 「+ 新建视图」 (plus 「管理视图」 for an admin) at the foot. 📌 on 线索看板 makes the bare address and the sidebar entry open it; unpinning restores 全部线索. ☆ puts 「object icon + view label」 into a 收藏 group at the top of the sidebar; clicking it opens the view, unstarring removes it, and a keyboard (Space + arrows) or mouse reorder holds across a reload. Views that an authored sidebar entry already opens (setup app: 全部用户, sessions, notifications) show ☆ disabled with 「已在菜单中」 and are not duplicated, and the CRM favorites do not show in the setup app. In split mode the list keeps the same switcher, and its dropdown opens and switches views with the list body dragged to ≈464 px and at 1280. No pageerror in any walk.

Target Verdict Screenshots
1 Title is the switcher, tab row retired met 01-list-title-1440.png, 07b-kanban-face.png
2 Dropdown: search, sections, order, icons, ☆/📌, + 新建视图 met 02-dropdown-open-1440.png, 03b-dropdown-row-hover-clip.png, 04-dropdown-search.png, 07-dropdown-sections-after-create.png
3 Pinned = default per object met 08-dropdown-pinned-kanban.png, 09-bare-address-opens-pinned.png, 10-bare-address-after-unpin.png
4 ☆ → sidebar 收藏, reorder, authored item not duplicated met 12-sidebar-favorites-one.png, 13-favorite-click-opens-view.png, 16-favorites-after-keyboard-reorder.png, 17-favorites-after-reload.png, 23-sidebar-after-unstar.png, 40b-inmenu-sys_user-clip.png
5 Split pane uses the same switcher met 30-split-pane-1440.png, 31-split-pane-dropdown.png, 35-split-pane-list-narrowed.png, 36-split-pane-narrowed-dropdown.png, 38-split-pane-1280-dropdown.png

Findings below the bar (none is a REWORK reason for this card):

Not checked: a chart face and a view with a description (the CRM app has neither); a published reader-saved view (the seed has none); reordering non-view favorites. The v4 boards predate this card (they still draw a tab row), so the reviewer used them only as a reference for the split layout.

Screenshots (48) and the reviewer's full report are in the project's files: ui-seat2/card-27594/preview-review/shots/ and ui-seat2/card-27594/preview-review/review.md. The inputs the reviewer got are in ui-seat2/card-27594/preview-review/inputs/.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head 16c257005a5b189a942a501486b82023579cba7a · 2026-10-30T11:06Z

  1. Shape: draft → main, Fixes #27594, attribution block on top, root changeset (@object-ui/app-shell, @object-ui/i18n patch), docs section in content/docs/ui/views.mdx. 51 files, +2206 / −560. No assignee, no reviewer.
  2. Card rule met (seat read the diff and the shots): Target 1 (the title is 「视图名 ▾ · 条数」, objectViewTabBar.tsx deleted, filter / sort / display keep their row), Target 2 (search always, 📌 固定 · 最近 · 全部, authored then saved, type icons, ☆ / 📌 on hover, 新建视图; 管理视图 for admins, the agreed addition), Target 3 (pin in workspacePreference list.view.pinned@object:NAME; address > pin > isDefault > first), Target 4 (seat ruling B 6513079106: stars in the existing favorites store, type view, drawn per app, sortable; a menu view's ☆ disabled 「已在菜单中」), Target 5 (the split pane keeps the same switcher). Shots 00–07 match. The breadcrumb premise miss is carded as Console object list: with the view title as the switcher, nothing on screen names the object once the sidebar is collapsed or the list is opened from a link — restore the object crumb (#27594 premise) #27950; the split-pane double count is Record drawer v4: a bounded peek over the list — md cap instead of the field-count bucket, no dim overlay, the list stays readable and clickable, ⤢ maximizes in place at the same URL; the full page is reached only from the row ↗ and the header ⋯ #27545 B's to fix.
  3. Gates (check-runs on this head): 51 success, 8 skipped, 0 red. Named runs: Lint & Repo Gates 123898235541, TypeScript Type Check 123898106465, Type Check UI 123897012269, Test UI 123900714266. Dev: 66/66 derived gates exit 0 (incl. check-sdui-manifest); five ablations each red and restored.
  4. Contract review: not owed. Clause-②: no (app-shell is private; i18n only adds keys), Metadata-protocol: none.
  5. Acceptance 3 (preview verification): ruled B (6520722066) on the maintainer's 「你可以自主验收合并,有问题会开新卡」 (2026-10-30T10:39Z). The card thread's context-free proxy preview review on this head: PASS (6521162189). The seat also read the dev's console-preview shots on examples/app-crm, which is the accepted surface; HotCRM is not required. The epic seat re-checks the result on main and files cards for any deviation.
  6. Landing: not governed (check-governed-merges --pr 27943: 0 of 51 paths), 2766 changed lines ≤ 3000. The seat lands it: ready + auto-merge now. Closes Console lists: the list-view switcher becomes a dropdown on the list title (search, pinned default, recent), the flat view-tab row retires, and a starred view appears in the sidebar's 收藏 group #27594.

Generated by Claude Code

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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant