Skip to content

fix(ui): the list flow at phone width: 44 px targets, inset panels, a card pager, the active sort on the bar, and Back that pops cleanly - #29273

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-0nuqvb
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-0nuqvb

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

Requested by Steedos · project thread

Fixes #29074
Fixes #29057

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:手机宽度(sm 以下)下,列表流程的每个控件都至少 44 px:工具栏的视图切换、筛选、排序、显示、搜索(含搜索框和清除),视图切换菜单,筛选面板,排序构建器,分组编辑器,以及显示面板的行、开关和底部按钮。筛选、排序、显示三个面板左右各留 16 px。排序层级和筛选条件的字段选择框占满整行,标签不再被截成「Accou…」。服务端分页时,手机卡片列表末尾多了一个翻页条(首页、上一页、第 N/M 页、下一页、末页),复用表格已有的文案。下拉刷新只在列表滚到顶部时触发。有排序时,工具栏保留排序按钮并显示层级数。手机上从列表打开的记录页,「‹」改为在历史里后退,之后再按返回会离开列表,不会重新打开记录;「‹」仍显示对象复数名。桌面尺寸和行为不变。
  • 为什么改:移动端 App 直接嵌入 Console。375 / 390 px 实测:工具栏按钮 24–28 px;筛选面板贴右边、显示面板贴左边;403 条记录的列表在手机上只显示前 50 条,没有任何翻页入口;滚到列表中间往下拖会弹出「Pull to refresh」并重新拉数据;排序移进显示面板后,排序生效时工具栏上看不到任何提示(Phone list: an active sort shows nowhere on the toolbar once Sort moves into the Display panel, and the sort builder truncates field labels at phone width #29057);从列表打开记录后点「‹」再按返回,会重新打开刚才的记录(fix(plugin-detail, app-shell, components): the record page's controls are 44 px touch targets at phone width, and Back after 「‹」 leaves the record #29210 的发现)。
  • 风险与回滚:低。大部分是 max-sm: 的 className,桌面不受影响。三处行为变化:手机列表推入记录页时带上 RECORD_PAGE_PUSHED_STATE 标记(替换式跳转不带);usePullToRefresh 在任何宽度都只在触点与绑定元素之间的滚动框都在顶部时才开始下拉;触屏设备上视图行的 ☆ 和 📌 一直显示(鼠标仍是悬停显示)。不加键、不加 prop、不加导出、不加文案。回滚即回退本 PR。
  • 先看:ui/packages/plugin-grid/src/ObjectGrid.tsx 卡片分支里的 cardPager(只在 manualPaginationOn 且总数超过一页时绘制,翻页走表格同一个 manualOnPageChange);ui/packages/mobile/src/usePullToRefresh.ts 的 scrolledBetween;ui/packages/plugin-list/src/ListView.tsx 排序按钮的隐藏条件(sortInDisplay && (currentSort.length === 0 || phoneSearchOpen));ui/packages/app-shell/src/views/objectViewRecordNavigation.ts 的 phone && !replace 标记,以及 RecordDetailView.tsx 里「‹」标签条件对列表来源保留复数名。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.phoneWidth-29074.test.tsx packages/plugin-grid/src/__tests__/phoneCardPager-29074.test.tsx packages/mobile/src/__tests__/usePullToRefresh.innerScroller-29074.test.tsx packages/components/src/__tests__/list-builders-phone-width-29074.test.tsx packages/app-shell/src/views/objectViewTitleSwitcher.phoneWidth-29074.test.tsx packages/app-shell/src/views/objectViewRecordNavigation.phonePush-29074.test.tsx,预期 6 个文件全部通过。
  • 需要你做的:无,席位落地。

Part of #29040. Claim: issuecomment-6572134252, surface amended in issuecomment-6572419569, issuecomment-6573926280 and issuecomment-6573994363.

What changes

The mobile app hosts the Console in a WebView, so below sm the Console is the phone UI. This PR covers the Lists flow: the list toolbar, the view switcher, Filter, Sort, Display, phone search, the card list and its end, and pull-to-refresh. Every change applies below sm (max-sm: utilities), so wider screens are unchanged, except where a bullet says otherwise.

No key, prop, package export, locale string or spec surface is added. The 22 px link of an embedded grid's first column is not touched; it is #29219.

Why

Measured on main (a4015ab) at 375 and 390 px on the CRM and showcase examples, every list control was 24–36 px; Filter ran flush right and Display flush left; records past the first page could not be reached on a phone; a downward drag mid-list refreshed; an active sort was invisible on the phone bar (#29057); and on a phone, Back after a list-opened record's 「‹」 opened that record again (#29210's run).

Tests

  • ListView.phoneWidth-29074.test.tsx (new): toolbar tokens; the Sort trigger is hidden below sm when unsorted, shown with its count when sorted, and hidden while phone search is open; the Display panel still carries the sort block; useBelowSm follows a matchMedia stub.
  • phoneCardPager-29074.test.tsx (new): 12 records at page size 5 and 390 px: the pager reads Page 1 of 3, Next shows records 6–10, Last shows 11–12 and disables Next; every button is h-11 w-11. Control: 3 records draw no pager.
  • usePullToRefresh.innerScroller-29074.test.tsx (new): control, a drag on a row with the inner box at its top pulls and refreshes; with the inner box scrolled the same drag shows no indicator and no refresh; a touch outside the scrolled box still pulls.
  • list-builders-phone-width-29074.test.tsx (new): SortBuilder, FilterBuilder and GroupingEditor tokens, desktop tokens kept beside them.
  • objectViewTitleSwitcher.phoneWidth-29074.test.tsx (new): trigger and menu tokens; ☆ and 📌 show on pointer-coarse and stay hover-only otherwise.
  • objectViewRecordNavigation.phonePush-29074.test.tsx (new): a phone row click carries the marker; a desktop row click and a phone's drawer-link replace do not; a plugin view's push does. On the real RecordDetailView at 390 px, home → list → row → 「‹ Leads」 (a button) returns to the list, and Back then lands on home. Control: an unmarked push makes 「‹」 a link, which since fix(plugin-detail, app-shell, components): the record page's controls are 44 px touch targets at phone width, and Back after 「‹」 leaves the record #29210 replaces the record's entry with a second copy of the list, so Back stays on the list and only a second Back reaches home. A lead pushed from another lead's page still reads 「‹ Mira Costa」. Ablating each of the three source edits on its own fails its cases (the list hook: 2; the plugin hook: 1; the label condition: 1), and the rest pass.
  • Ablation (one time, not kept): with origin/main's eight source files copied in (ListView.tsx, ViewSettingsPopover.tsx, objectViewTitleSwitcher.tsx, sort-builder.tsx, filter-builder.tsx, grouping-editor.tsx, usePullToRefresh.ts, ObjectGrid.tsx), the first five new files went 15 failed and 5 passed. The five that passed are the controls (unsorted list hides Sort, Display keeps the sort block, a pull at the top, a touch outside the scrolled box, a one-page list draws no pager). The restore left git status clean.

Acceptance notes

The card's run record, measured in a real browser. The Console is a production build of this branch (pnpm console:build), served at /_console by objectstack dev --seed-admin --fresh. Examples: CRM (crm_account) and showcase (showcase_account), each seeded with 400 extra accounts (403 rows). Chromium with isMobile and hasTouch at 375×812 and 390×844, DPR 2. The before run is on main a4015ab, the after run on this branch at 9f27c26. Header chrome (AppHeader) is #29075's and is left out of the touch census.

Check 375 CRM 375 showcase 390 CRM 390 showcase
No horizontal page scroll (list, menus, panels, search, list end, page 2) pass / pass pass / pass pass / pass pass / pass
Touch targets ≥ 44 px (toolbar, switcher menu, Filter, Display, Sort, search, list end) fail / pass fail / pass fail / pass fail / pass
Dialogs and panels fit the viewport pass (flush to an edge) / pass (16 px each side) same same same
Field labels shown whole (Filter, Sort) fail / pass fail / pass fail / pass fail / pass
Virtual keyboard does not cover the focused field (filter value, phone search) pass / pass pass / pass pass / pass pass / pass
Records past page 1 reachable from the card list fail / pass fail / pass fail / pass fail / pass
Pull-to-refresh only at the top fail / pass fail / pass fail / pass fail / pass
Active sort shown on the bar (#29057) fail / pass fail / pass fail / pass fail / pass
Long lists responsive at 4× CPU pass / pass pass / pass pass / pass pass / pass
Search, card tap, FAB clears the last card pass / pass pass / pass pass / pass pass / pass
Row actions reachable from a phone card fail / fail fail / fail fail / fail fail / fail

Each cell reads before / after. The Back fix came in after this run (from #29210's findings). It was checked live on the rebuilt production console at 375 and 390 (CRM, home → lead list → first card → 「‹」 → browser Back): 「‹」 is a button reading 「Leads」, it lands on the list, and Back then lands on home rather than the record.

  • Toolbar at 375 (CRM), before → after: view switcher 114×24 → 114×44; Filter 28×28 → 44×44; Display 28×28 → 44×44; Search 28×28 → 44×44; open search field 144×28 → 144×44; Clear 28×28 → 44×44; Sort (with an active sort) not shown → 48×44.
  • Panels at 375: Filter left 32 / right 375 → 16 / 359; Display left 0 → 16 / 359; Sort 16 / 359.
  • Long list: Next on the card pager shows "Page 2 of 9" with the scroller back at the top, on all four runs.
  • Pull-to-refresh: scrolled 600 px down, a downward drag showed "Pull to refresh" and refetched on main; on this branch it shows nothing and makes no request. At the top it refetches once, as before.
  • 4× CPU (CDP Emulation.setCPUThrottlingRate, production build): first card 3.2–4.4 s, 40 scroll frames p95 17–20 ms (max 25) with no long task, search typed to result 352–509 ms. On main: 3.2–3.8 s, p95 18–20 ms, 450–680 ms. The CRM and showcase runs of one width shared the box.
  • Full inbox page (sys_inbox_message?view=mine, 60 rows, 4× CPU; Console at phone width: Notifications flow audit and fixes at 375 and 390 px #29076 recorded a 284–495 ms first-scroll frame there): the first visit in a fresh browser context reaches network idle (about 2 s) before the card list has mounted, and the next frames carry the mount, 279–341 ms with 245–299 ms long tasks. Once the list is there, every scroll frame is 18–36 ms with no long task, scrolled at once or after 1.5 s (11 runs). The long frame is the page's first render, not scrolling; this PR does not change that render.
  • Out of scope, measured by fix(plugin-detail, app-shell, components): the record page's controls are 44 px touch targets at phone width, and Back after 「‹」 leaves the record #29210's phone run: opening a record's 151-row related tab takes 176–392 ms at 4× CPU, on main and on this branch alike. It is a render cost of the embedded grid (plugin-detail RelatedList tab activation, then plugin-grid ObjectGrid), not a phone-layout fault, and needs a profile to place.
  • Still failing, carried by another card: a phone card has no row-action control, and a long press opens the record. Fixing it changes the phone card layout of console phone list: card rows draw every shown column as a label / value line — the board's row is the name, one subtitle line and one status chip #28484, so seat 1 filed it as its own card.
  • Before and after screenshots of each step at both widths are in the project folder, as the card asks. The seat links them.

Verification

Readings on head cc0e7090 (merged with main at eff2d2a9, which brought #29210's replacing 「‹」 link and #29252's Display footer; the footer conflict kept main's layout plus this PR's phone sizes). At cc0e7090: the five packages' type-check 69 of 69; plugin-list and mobile whole plus every new file, the census, the components dialog and builder files and the record-page Back suites, 245 files, 2402 of 2403 tests passed, the one failure being the Back file's stale control, since corrected; the corrected Back file, shuffled with its neighbours under a concurrent load, 20 of 20 runs green, and ablating the list hook's marker still fails its 2 cases. Earlier: The package suites ran at 6641c3be (main with #29108 merged), before the Back commit; that commit's app-shell files were then checked with their own suites, and the derived sweep re-ran at ea6d9657. Merging main again (5692ba8d, #29189 edits ListView's imports; a one-line import resolution) was followed by the five packages' type-check (69 of 69) and a re-run of plugin-list and mobile whole plus every new file and the record-page Back suites: 204 files, 1931 tests passed.

Tests
  • plugin-list, plugin-grid and mobile whole, every components test file that names the sort, filter or group builder (29 files, the new one among them) and the three app-shell files that name the view switcher, under the verify lock with --maxWorkers=2: 496 files and 4664 tests passed. This is a narrowing for components and app-shell; CI's Test UI runs the rest.
  • The Back commit: the new objectViewRecordNavigation.phonePush-29074.test.tsx with RecordDetailView.phoneFace-27058, RecordDetailView.topBar-27524, recordPagePush-27058, objectViewRecordNavigation.listRows-27524 and recordPaneGroupScope-28461: 6 files, 45 tests passed.
Type-check and lint
  • turbo run type-check for @object-ui/plugin-list, plugin-grid, components, app-shell and mobile (both tsc legs, src and tests) on the merged head: 69 of 69 tasks passed; @object-ui/app-shell again at ea6d9657: exit 0.
  • eslint (from ui/) on the 17 changed source and test files: 0 errors. With --no-inline-config it reports one error, react-hooks/static-components at ObjectGrid.tsx DataEmptyState, which main already carries with an eslint-disable-next-line; no warning is new apart from react-refresh/only-export-components on the three ViewSettingsPopover exports.
  • pnpm check:new-line-citations under ui/: 0 new citations.
Derived gates

node scripts/pm/dispatch-gates.mjs --ran: 35 derived families accounted for, 35 run, 0 NOT MEASURED, every one exit 0, the whole sweep re-run at ea6d9657. That includes node scripts/check-sdui-manifest.mjs and its --self-test, pnpm --dir ui exec vitest run scripts/ (169 files passed, 2 skipped; 4900 tests passed) and pnpm check:type-check-debt under the verify lock. The PR-body gates (check-changeset-no-major, check-closing-keyword-parity, check-adr-0087-registration, check-empty-changeset, check-partof-closing-keyword) ran on this body.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; lint passes on the changed files.
  • 4. One issue, one branch off an up-to-date main, one pull request. (Phone list: an active sort shows nowhere on the toolbar once Sort moves into the Display panel, and the sort builder truncates field labels at phone width #29057 is folded in by the card's claim amendment.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section is the maintainer summary the lane asks for).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jUwDX3ZR6eBujakKoT

…els, a card pager, pull-to-refresh only at the top

WIP: toolbar, view switcher, filter, sort and grouping builders, Display
panel, the phone card list's pager and the pull-to-refresh guard.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUwDX3ZR6eBujakKoT
…a third named pager

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUwDX3ZR6eBujakKoT
…er commits it in a transition

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUwDX3ZR6eBujakKoT
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

席位核对 · ACCEPT · 2026-11-02T12:36Z

Head-sha: cc0e7090a61f5d4cdca21ecb9ab69305b9948bcb

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 46e2400 Nov 2, 2026
60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0nuqvb branch November 2, 2026 13:18
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

1 participant