Skip to content

fix(ui): KPI tiles sit two across on a phone, the calendar footer counts its loaded dates, and record not-found names no access - #28060

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

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

Requested by Bill · project thread

维护者速读

  • 改了什么:手机上首页的 4 个 KPI 卡片改为每行两张;日历视图底部计数改称「为这些日期加载的 N 条记录」;记录打不开时的提示改为「不存在、可能已删除,或你可能没有访问权限」。三处都是界面层改动,十个语言包同步。
  • 为什么改:HotCRM 18.2.0 全量走查里,手机首页的四张 KPI 各占一行占满首屏;日历底部的数字其实只是当前日期窗口里的记录,却和列表的「总数」用同一句话;经理打开无权查看的商机时被告知「不存在或已删除」,而服务端对无权读和真不存在都回 404,界面分不出两者。
  • 风险与回滚:KPI 改动作用于所有「裸数字 columns 为 4 及以上」的页面网格在手机宽度(sm 以下)的列数,桌面和平板不变,作者写了响应式对象的网格不变;计数只在日历按日期窗口取数时换文案,表格、看板和内联数据的计数原样;文案改动只改两个键的值。回滚即还原本 PR。
  • 先看:ui/packages/components/src/renderers/layout/grid.tsx(裸数字的手机降级那段);ui/packages/plugin-list/src/ListView.tsx(record-count-bar 里的 calendarWindowed 分支);ui/packages/i18n/src/locales/en.ts(新键 list.calendarRecordCount 与改值的 recordNotFoundDescription)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/components/src/__tests__/grid-phone-tiles-27355.test.tsx packages/plugin-list/src/__tests__/ListView.calendarFooterCount-27355.test.tsx packages/app-shell/src/views/RecordDetailView.recordReadFailure-11902.test.tsx,预期 3 个文件全绿。
  • 需要你做的:无,席位落地。

Fixes #27355
Clause-②: no
Metadata-protocol: none

What changes

Rows 7, 8 and 11 of #27355, re-measured on origin/main at 3f02da1. Rows 1 and 5 are answered in the Acceptance notes and this PR does not touch them.

  • Row 11, KPI tiles on a phone. A layout grid with a bare columns count used to ramp to one column below sm, so a home page's KPI strip (columns: 4 over object-metric tiles, the same shape as the showcase home and the schema catalog's report/report-header-with-kpis) took a row per tile and filled a phone's first screen. A bare count of four or more now draws two across below sm and the authored count from md (grid grid-cols-2 md:grid-cols-4). That matches the dashboard's own phone layout, which already puts metric tiles two across (DashboardRenderer's mobile branch). A bare 2 or 3 still stacks on a phone and pairs from sm, a bare 1 is one across everywhere, and an authored breakpoint object is drawn as written, { xs: 1, md: 4 } included.
  • Row 11, the page tab strip. Already done on main: page:tabs scrolls sideways with an edge fade on the side that hides a tab (useHorizontalOverflow + data-overflow mask in containers.tsx), landed by fix(app-shell): Setup walkthrough rows — one name for Setup, confirms before Disable and before switching a packaged action off, read-only reasons, forced-reset wording #27849 after 18.2.0. Measured: the 18.2.0 release commit's containers.tsx has no stripRef, the fix(app-shell): Setup walkthrough rows — one name for Setup, confirms before Disable and before switching a packaged action off, read-only reasons, forced-reset wording #27849 merge has it.
  • Row 8, the calendar's footer count. A calendar hosted by a list view fetches the date window it draws (calendarWindowed), so the footer's data.length is that window's records, not the view's. It read N records, the words the grid uses for the view's total. It now reads N records loaded for these dates (new count family list.calendarRecordCount, every CLDR slot in all ten packs, mirrored in LIST_DEFAULT_TRANSLATIONS). Counting the view instead would need a second query without the window; the label is what the existing data path supports. A grid, a board and a calendar over rows handed in whole keep N records.
  • Row 7, the record not-found copy. getData reads the record with the caller's context and throws recordNotFoundError when the read finds nothing, so a row the reader may not see answers 404 RECORD_NOT_FOUND exactly like a missing one, and the page cannot tell them apart without the server revealing existence. empty.recordNotFoundDescription and detail.recordNotFoundDescription now say "The record you are looking for does not exist, may have been deleted, or you may not have access to it." in all ten packs, and DETAIL_DEFAULT_TRANSLATIONS follows. The record page's inline fallback (RecordDetailView.tsx) follows too. A refused read (403) still shows the access-denied state, unchanged.
  • Changeset: @object-ui/components, @object-ui/plugin-list, @object-ui/plugin-detail, @object-ui/i18n patch.

Tests

  • ui/packages/components/src/__tests__/grid-phone-tiles-27355.test.tsx: 4, 5, 6, 8 and 12 across draw two on a phone and the authored count from md; controls: 1, 2 and 3 across unchanged, an authored breakpoint object unchanged (three shapes, { xs: 1, md: 4 } among them).
  • ui/packages/plugin-list/src/__tests__/ListView.calendarFooterCount-27355.test.tsx: a windowed calendar counts the 12 records of its month (not the 42 of the object) as 12 records loaded for these dates, and one record in the singular; controls: a grid over the same rows reads 42 records, and a calendar over rows handed in whole reads 42 records.
  • Existing pins updated to the new ramp: grid-breakpoint-columns-7097, grid-flat-columns-retired-11505, grid-dom-attribute-whitelist, schema-catalog grid-columns-key (three columns: 4 examples), and the not-found constant in RecordDetailView.recordReadFailure-11902 (its 403 and failed-load cases still assert no "may have been deleted").
Runs (head 8686ef1 unless named)
  • Targeted set on the final head (check-i18n-call-site-keys, DetailView, the five grid pins, schema-catalog grid-columns-key, both calendar footer/window files, RecordDetailView.recordReadFailure-11902, RecordDetailView.deleteConfirmDialog-11001, defaults-maps-mirror-en-pack, all of packages/i18n/src/__tests__/): Test Files 104 passed (104), Tests 1675 passed | 13 skipped (1688).
  • Affected packages whole, at 40a769c before the merge of main (packages/components/, packages/i18n/, packages/plugin-list/src/__tests__/, packages/plugin-detail/src/__tests__/, examples/schema-catalog/, plus the app-shell files above): Test Files 977 passed | 2 skipped, one red (DetailView asserting the old sentence), fixed in 40a769c and green since.
  • Ablation 1 (scripts/ablation-replace.mjs, anchor if (baseCols >= 4) { replaced by if (false) {, i.e. the old ramp; the test resolves grid.tsx from source): grid-phone-tiles-27355 Tests 5 failed | 3 passed (8), the 5 = every four-or-more pin, the 3 = the controls; restored blob == HEAD 41b7636b1aa5, git diff HEAD empty.
  • Ablation 2 (anchor ? t('list.calendarRecordCount', { count: data.length }) replaced by the list.recordCount call, i.e. the old words over the same count; ListView resolves from source): ListView.calendarFooterCount-27355 Tests 2 failed | 2 passed (4), the 2 = the calendar label pins, the 2 = the grid and inline-data controls; restored blob == HEAD 8866406e8d92, git diff HEAD empty.
  • pnpm --filter @object-ui/components --filter @object-ui/plugin-list --filter @object-ui/plugin-detail --filter @object-ui/i18n run type-check: exit 0 (each runs tsc --noEmit && tsc -p tsconfig.test.json).
  • pnpm run lint in components, plugin-list, plugin-detail, i18n: exit 0, 0 errors.
  • node scripts/check-i18n-en-drift.mjs (ui): exit 0, 2 en values changed, 18 pack values followed.

Acceptance notes

Per-row disposition of #27355 rows 1, 5, 7, 8 and 11, re-measured on origin/main at 3f02da1 (rows 2–4, 6, 9–10 and 12–13 are answered on the card by other seats):

Row Bucket Reason
1 · path steps cannot be clicked CARD (spec, then ui) record:path is a readout by design: its header comment records that it carries no handler and that click-to-advance is a separate feature, and its only channel, the record context, exposes no record-field write. The server side already exists (an update is judged by the object's state_machine validation rule, ADR-0020), so the card is a spec opt-in on RecordPathProps plus a write channel and confirm in the renderer. Filed by the seat as #28061.
5 · the record page has no Delete ALREADY DONE (generic path) The record header's ⋯ menu ends with Delete (sys_delete in RecordDetailView's synthesized system actions, present in the 18.2.0 release commit too). It is gated by the same delete affordance row (recordDelete and rowDelete are both crud: delete, grant: delete in affordanceGrants.ts) and the same per-record verdict as the list row menu, so a reader offered Delete in the list row is offered it in the header. RecordDetailView.deleteConfirmDialog-11001 passes on this head. If HotCRM can name an object, role and page where the header lacks it, that is a new measurement;
7 · "does not exist or has been deleted" for an unreadable record TAKE This PR (copy names both).
8 · calendar footer counts the loaded window TAKE This PR (label names the window).
11 · KPI tiles a row each; tab strip overflows TAKE (tiles) / ALREADY DONE (tab strip) This PR for the tiles; the strip by #27849.

RecordDetailView.tsx changes by one line: the inline defaultValue of empty.recordNotFoundDescription follows the new en sentence, which check-i18n-call-site-keys (default-value drift) requires. No other line of that file is touched.

Verification

Local gate subset derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this diff, each recorded with its exit code before any pipe and reconciled with --ran; every family CI runs is read from this PR's check-runs.

Gate run (head 8686ef1)
  • dispatch-gates --commands: 34 commands, every one :: exit 0, among them node scripts/check-sdui-manifest.mjs and its --self-test, pnpm check:type-check-coverage, pnpm check:type-check-debt, pnpm check:nul-bytes and pnpm --dir ui exec vitest run scripts/ (Test Files 169 passed | 2 skipped (171)).
  • dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
  • Outside the derivation: pnpm --dir ui run check:new-line-citations exit 0 (0 new citations).
  • CI check-runs: read by the seat.

Generated by Claude Code

…indow count, not-found copy names no access

A bare layout-grid count of four or more keeps two tiles across below sm;
a windowed calendar's footer reads "N records loaded for these dates";
the record not-found description says the record may also be unreadable,
since the server answers both with 404 RECORD_NOT_FOUND.

Claude-Session: https://claude.ai/code/session_01Utp6CXNi3EtSwLHRSornGu
Co-authored-by: Claude <noreply@anthropic.com>
…schema-catalog pins

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

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

席位核对

ACCEPT · head 8686ef1130d199258bf68c18016ad594227ba4bb · 2026-10-30T11:28Z

  • Shape: draft → main, now Fixes #27355. The seat patched the body from Part of: row 1 is owned by Record path: a stage can be clicked to request that status, judged by the field's validation and state machine #28061 in domain:spec, and every other row is answered on the card or here. Clause-②: no and Metadata-protocol: none match the claim. The change touches only ui packages (components, plugin-list, plugin-detail, app-shell, i18n), so no contract review is owed.
  • Row 11 (TAKE): a layout grid with a bare columns of 4 or more now draws two across below md (it used to be one per row below sm), so a phone shows a KPI strip two by two. 1–3 columns and responsive-object columns are unchanged. A control pins columns: 3 → grid-cols-1 sm:grid-cols-2 md:grid-cols-3. Four existing grid tests are re-expected to the new class string for the 4-column case only. The page tab strip is ALREADY DONE by fix(app-shell): Setup walkthrough rows — one name for Setup, confirms before Disable and before switching a packaged action off, read-only reasons, forced-reset wording #27849.
  • Row 8 (TAKE): a calendar fetched by date window labels its footer "N records loaded for these dates" (a new list.calendarRecordCount family, every CLDR slot in all ten packs). Grid, kanban and inline-data calendars keep "N records", and a control pins it.
  • Row 7 (TAKE): getData reads as the caller and answers 404 RECORD_NOT_FOUND for a row the caller cannot read, so the console cannot tell "no access" from "gone" without the server revealing existence. Both recordNotFoundDescription strings now say "…does not exist, may have been deleted, or you may not have access to it." in all ten packs, and RecordDetailView's defaultValue follows. That is one line, which the seat allowed; it does not overlap feat(rest,console): one request opens a record page #27713's hunks.
  • Row 5 (ALREADY DONE on the generic path): the record header's ⋯ menu ends in sys_delete, behind the list row menu's gate and per-record verdict, since 18.2.0. HotCRM's own page was not measurable here; it is recorded as an observation to reopen with the object, role and page if it reproduces.
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure; mergeable_state clean. 34/34 derived gates exit 0 locally; both ablations turn red. check-governed-merges --pr 28060: NOT governed, 382 changed lines.
  • Accepted deviations: the file surface grew by plugin-list/src/ListView.tsx (the calendar footer lives there), components/.../layout/grid.tsx, plugin-detail/useDetailTranslation.ts and the one RecordDetailView.tsx line. No other open PR touches any of them, except feat(rest,console): one request opens a record page #27713 on non-adjacent hunks.
  • Landing: non-governed, all checks green, mergeable_state clean ⇒ the seat lands it (pr_ready + automerge_enable).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 11:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 11:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit f0857d2 Oct 30, 2026
60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xgi39j branch October 30, 2026 11:48
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/m tooling

Projects

None yet

1 participant