Skip to content

feat(console): report index groups reports by object, with All/Recent views and search - #27622

Draft
objectstack-fleet[bot] wants to merge 6 commits into
mainfrom
claude/project-thread-f6jl7y
Draft

objectstack-fleet[bot] wants to merge 6 commits into
mainfrom
claude/project-thread-f6jl7y

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread

Refs #27526

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:应用的报表索引页从一张平铺列表改为按报表主对象分组(导航顺序,「其它」垫底,组头带计数可折叠),顶部加「全部 / 最近」两个视图(按用户记住选择)和搜索框;每行显示报表名、所属对象、上次打开时间。
  • 为什么改:维护者裁决「索引页按对象分组 + 最近/我的/全部」;报表多了以后平铺列表找不到东西。「我创建的」和行内「所有者」没做:服务端下发的报表里没有创建者字段,详见 Acceptance notes,待你定。
  • 风险与回滚:只动 Console 的索引页和十个语言包,不动 spec、不动服务端、不加元数据键;回滚即 revert 本 PR。视图偏好存在浏览器 localStorage 的 os:pref:reports.index.view。
  • 先看:ui/packages/app-shell/src/views/appReportIndex.ts(主对象推导 reportPrimaryObject、分组 groupReportsByObject、偏好声明)→ AppReportIndexView.tsx(视图与搜索)→ 新测试 AppReportIndex.groupsViews-27526.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx,预期 17/17 通过。
  • 需要你做的:决定「我创建的」与所有者列的数据来源(见 Acceptance notes 的选项 A/B),其余无,席位落地。

What changes

The app's report index (/apps/APP/reports, AppReportIndexView) listed every report the member may open as one flat list. It now follows the card's Target 1–3, except the 「我创建的」 view and the row's owner, which have no data source (see Acceptance notes).

  • Grouped by primary object, derived, no new key. reportPrimaryObject in appReportIndex.ts takes the object of the first dataset the report binds, in reportDatasets order (its own dataset, then each blocks[].dataset), that the dataset list maps to an object. Rule chosen for a report with several datasets: the first dataset that resolves to an object, so a joined report whose first block names an unknown dataset still lands under the object of its next block. A report with none goes to the 「其它」 section.
  • Section order. groupReportsByObject: first the objects the app's served navigation names (type: 'object' entries, in the order flattenNavigation first meets them), then the objects it does not name, by label, then 「其它」. Each section header is a button with aria-expanded that shows the count and collapses its section (visit-only state).
  • Views. A view switch with two buttons, All reports (全部) and Recent (最近). The choice is REPORT_INDEX_VIEW_PREFERENCE, declared through defineWorkspacePreference in the user scope, stored as {"value":"all"|"recent","v":1} under os:pref:reports.index.view; a reader who never chose, or whose stored value the codec does not read, sees All reports. Recent reads the recent-items store (useRecentItems, the report entries and their visitedAt) and lists the index's reports the reader opened, most recent first, as one flat list: the order is the point of that view, so it is not split by object (my reading of Target 1 + 2; the sections apply to All reports). A recent report the index does not list (another app's, an unreadable one) is not shown.
  • Rows. A row is the report's link (label and description, so the link's accessible name stays the report's label, which the existing index pins rely on), its object's label and when the reader last opened it (timeAgo in the display locale), with the link's overlay covering the row. The object and time cells hide below sm.
  • Search. A search box filters the current view by report label, description or object label, case-insensitively; a view with no match says so.
  • Object labels. useReportObjectLabels labels the sections (plural) and rows (singular) from the object definitions the shell reads for the app: the app's package object scope, the same readObjectScope read the navigation gates already make, plus the grouped objects outside the package by name; an app with no package reads those objects by name. The index still never reads the whole object registry (the existing CONTROL pin holds, and a new one beside it).
  • i18n. Nine new console.reportIndex.* keys in all ten packs: viewsLabel, viewAll, viewRecent, searchPlaceholder, searchLabel, otherSection, lastOpened, recentEmpty, noMatch.

The ⌘K palette reads the same useAppReportEntries; its entries gain an optional object and are otherwise unchanged.

Why

Maintainer ruling on the card, quoted verbatim: 「ui(索引页按对象分组 + 最近/我的/全部)」. A flat list stops working once an app ships more than a handful of reports.

Tests

New ui/packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx (17 tests), through the real MetadataProvider, MePermissionsProvider, ExpressionProvider, RecentItemsProvider and I18nProvider:

  • grouping: sections in navigation order (Opportunity before Lead although L sorts first), an object the navigation does not name next, 「其它」 last, with counts; a joined report goes under the first of its datasets that resolves; the row's object cell; a header collapses only its own section; labels read from the package scope and never the whole registry;
  • views: All reports with no stored choice; Recent lists opened reports most recent first, only the index's, with "Opened N hours ago"; the choice is written as {"value":"recent","v":1} and read back by a fresh mount; an unreadable stored value is no choice; Recent with nothing opened says so; no 我创建的 button;
  • search: within All reports by label (empty sections dropped, counts recomputed) and by object label; within Recent only among opened reports; the no-match message.

The three existing index files (AppReportIndex-13020, .navContributions-13273, .navGates-26892) pass unchanged.

Run at head ac7be1f1 (after the merge of main):

  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/views/__tests__/ packages/app-shell/src/__tests__/ plus the ⌘K palette, AppContent, AppHeader, sidebar and route-chunk files that read useAppReportEntries or the console routes (58 named files): Test Files 162 passed (162), Tests 1514 passed (1514). The rest of the app-shell suite is CI's Test UI; it was not run here.
  • pnpm --filter @object-ui/i18n test: 89 files, 1374 tests passed (the packs' key-parity pins included).
  • Four ablations, each through scripts/ablation-replace.mjs (restored and proved restored): navigation order replaced by label order, 2 tests red; primary object taken from the first dataset only, 3 red; the view held in useState instead of the preference, 2 red; Recent listing every report, 3 red.

Acceptance notes

  • Needs a decision: 「我创建的」 and the row's owner have no data source. Measured on the server code that serves /meta/report (origin/main 556f3230): the served list item is the stored row's parsed body plus _packageId (storedOverlayEntries in packages/metadata-protocol/src/protocol-stored-overlay-reads.ts); the row's created_by column is read into MetadataRecord.createdBy by the database loader but never reaches a served item; METADATA_READ_DECORATIONS is _diagnostics and _draft only; ReportSchema declares no owner or creator key; and a code-authored report (every HotCRM report) has no creator at all. Per the dispatch, no key was invented here, the view and the column are left out, and this PR says Refs #27526. Options for the maintainer, with the seat's four-axis write-up in the report comment: A serve the stored row's creator as a read-time decoration on metadata reads (a spec + metadata-protocol card in the engine lane), then a small ui follow-up adds 我创建的 and the owner cell; B drop both from the ruling (code-authored reports never have a creator).
  • Recent is one flat list, not grouped, and sections apply to All reports: my reading of Target 1 with Target 2; say so if the sections should also split Recent.
  • The primary-object rule is the first dataset that resolves to an object, not strictly the first dataset (see What changes).
  • Last-opened time comes from the recent-items store, which keeps the reader's last 20 targets of every kind: a report opened long ago shows no time and is not in Recent.
  • Docs. ui/AGENTS.md ✨ Set up Copilot instructions #2 asks a feature to update the package README and its content/docs/frontend/ page; no docs page describes the report index today and the dispatch's file surface excludes docs, so none is written here.
  • Screenshot. The screenshot attempt (HotCRM is outside this session's repository scope, so the attempt boots examples/app-showcase) and its result are recorded in the os-dev-report comment on Report index page: group reports by their primary object (derived, no key) and offer 最近 / 我创建的 / 全部 views #27526.

Verification

Typecheck and lint (head `ac7be1f1`)
  • pnpm --filter @object-ui/app-shell type-check: exit 0, after rebuilding the @object-ui/app-shell^... closure the merge moved; the new test file is in tsconfig.test.json's program (--listFilesOnly count 1).
  • pnpm --filter @object-ui/i18n type-check: exit 0.
  • eslint on the three touched app-shell files: 0 errors. Its 5 warnings are no-explicit-any: 3 on lines of appReportIndex.ts that main already has, 2 on the getObjectSchema test double the new file copies from AppReportIndex-13020.test.tsx.
  • pnpm --dir ui check:new-line-citations: 0 new citations.
Derived gates

At head ac7be1f1, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 derived (the same list as at the merge head b5723584); each run and recorded, reconciled with --ran: "34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED". pnpm --dir ui exec vitest run scripts/ timed out in 3 files under the concurrent gate loop and passed when run alone (Test Files 169 passed, 2 skipped); the line below is the solo run.

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 0
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: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

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the touched files: 0 errors; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

The view preference's docblock named the store by file name, which the
new-line-citations check reads as a cross-file address. Name the
declaring helper instead.

Claude-Session: https://claude.ai/code/session_01Utp6CYhwxF1tGkRRemmhXK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling labels Oct 29, 2026
… text

On a running console the cell cut "Opened 4 seconds ago" to
"Opened 4 seconds a...": eight rem is narrower than the English string
at the row's text size. Widen it to twelve rem, keep the truncation for
longer translations, and carry the whole text as the cell's title.

Claude-Session: https://claude.ai/code/session_01Utp6CYhwxF1tGkRRemmhXK
Co-authored-by: Claude <noreply@anthropic.com>
useReportObjectLabels reads getItemsByType('object') when the app's
object scope cannot be read, so the objectui#12811 census counts it as
a reader of the whole object list and failed on the unclassified file
(Test UI 5/16 on ac7be1f). It reads readObjectScope on the open path,
like useServedViewItems and useObjectPresence: add it as a scope row.

Claude-Session: https://claude.ai/code/session_01Utp6CYhwxF1tGkRRemmhXK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the tests label Oct 29, 2026

This branch has not been deployed

No deployments
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

Development

Successfully merging this pull request may close these issues.

1 participant