Skip to content

feat(plugin-report): the report page exports the drawn table as CSV / XLSX (exportOptions) - #27089

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-p301jn
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-p301jn

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Refs #25873 (item: the report page reads exportOptions)

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:报表页右上角新增「导出」按钮,可把页面上画出来的那张表(含合计行/合计列)下载为 CSV 或 XLSX;exportOptions 从 planned 翻为 live。
  • 为什么改:裁决 6408567749 选 B(时间范围 + 导出);spec 半边 spec(report): a report declares a dataset-dimension date range and export options #26548 已发布 exportOptions,但没有渲染器读它,报表无法带去开会。
  • 风险与回滚:只影响非 joined 报表页的表头一行;权限沿用列表的 export 操作判定,无提供者时照列表默认放行;回滚 = revert 本 PR。
  • 先看:ui/packages/plugin-report/src/ReportExport.tsx(权限判定与文件写出),DatasetReportRenderer.tsx 两处快照块,packages/spec/liveness/report.json 的 exportOptions 子树。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx,预期 11 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • New module ui/packages/plugin-report/src/ReportExport.tsx. ReportExportControl draws an Export popover offering the declared formats (absent = CSV and XLSX in that order, formats: [] = no control). It reads the report's exportOptions, label and name.
  • What is exported is the table as drawn. DatasetReportTable (tabular / summary) and DatasetMatrixTable build a snapshot of exactly the cells they render (header labels, rows in layout order, server totals row and totals column) and hand it up through ReportTableSnapshotContext, keyed on content, not identity (ui AGENTS chore: version packages #10). The download re-queries nothing and re-aggregates nothing, so it inherits the table's own runtimeFilter and, once feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 lands, its period.
  • Who may export. The control is drawn only when the viewer's effective API operations on the dataset base object (the query answer's object) include export, the list toolbar's predicate, read through usePermissions().getObjectApiOperations from @object-ui/permissions. An absent set (no provider, an unrestricted object) keeps it, as on the list.
  • Files. CSV: drawn text, UTF-8 with a byte-order mark, RFC 4180 quoting, CRLF rows; a dimension cell opening with a formula character gets the tab guard ReportExportEngine uses (a formatted measure keeps its minus); the tab guard applies to data cells and to every header cell (a matrix's across headers are data values). XLSX: one sheet named for the report, header bold, measure cells as numbers, dimension cells as drawn text; ExcelJS is import()ed only when an XLSX is asked for. File name = buildExportFileName from @object-ui/core (the list export's builder): fileNamePrefix, else the report label, else the name, plus a local timestamp. includeHeaders: false drops the header row.
  • Not read on joined (the spec refuses exportOptions there): the joined branch mounts no snapshot context, so its blocks publish nothing and no control is drawn.
  • Dependencies. @object-ui/plugin-report now depends on @object-ui/permissions (workspace:*) and exceljs (^4.4.0, the version @object-ui/plugin-grid already pins); pnpm-lock.yaml gains the importer lines only (+6).
  • Liveness. packages/spec/liveness/report.json exportOptions subtree flipped to live citing ReportExport.tsx#ReportExportControl, authorWarn / authorHint dropped in the same edit; state-counts/report.md regenerated (gen:liveness-counts).
  • Lint liveness pin (packages/lint/src/lint-liveness-properties.test.ts, the spec(report): a report declares a dataset-dimension date range and export options #26548 block, seat-accepted deviation): rewritten as a silence pin for exportOptions with an anti-vacuity read of the live row; dateRange still warns in the same call, so the ledger is proven loaded.
  • Changeset .changeset/25873-report-export.md: @object-ui/plugin-report patch.

Why

Ruling 6408567749 on #25873, letter B: "A, plus CSV / XLSX export of the report table through the existing list-export permission". The spec half shipped in #26548 (PR #26628); until this PR no renderer read exportOptions, so the key was inert.

Tests

New ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx, 12/12:

  • CSV of a summary report equals the rendered table cell for cell (header row, rows, Total row), formula guard on the data cell only, file name from the label.
  • absent formats offers CSV and XLSX; formats: ['csv'] offers only CSV; formats: [] draws no control.
  • a viewer whose apiOperations on the base object exclude export sees no control; CONTROL: same provider with export shows it; CONTROL: no provider shows it.
  • includeHeaders: false drops the header row; fileNamePrefix names the file.
  • XLSX parsed back with ExcelJS: sheet name, header, measures as numbers, total row.
  • a matrix CSV equals the rendered cross-tab, totals column and row included.
  • a matrix across-axis value opening with a formula character is guarded in the CSV header row.
  • a joined report draws no control.

Ablation (at 54aa045, node scripts/ablation-replace.mjs): deleting the predicate line in ReportExportControl (anchor x1 to x0, blob 7eae13a4 to b22beff9) turns exactly 1 of 11 red, the apiOperations refusal test; restored blob 7eae13a4 equals the HEAD blob, git diff HEAD empty. No dist leg: the test imports the renderer by relative path.

Acceptance notes

Verification

Head 858f0fe5:

  • pnpm --filter @object-ui/plugin-report run type-check && pnpm --dir ui exec vitest run packages/plugin-report/ && pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts under the verify lock: VERDICT command-exit 0; plugin-report 29 files, 269 passed / 7 skipped; lint 110/110.
  • node scripts/pm/dispatch-gates.mjs --commands: 83 families; --ran reconciliation: 82 run, all exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET: not every package has dist/ locally; CI runs it). First readings that were red and repaired on this branch: pnpm --dir ui exec vitest run scripts/ (one-authority-per-exported-name: ReportExportFormat collided with @object-ui/types, renamed ReportTableExportFormat), check:liveness (state counts stale, regenerated), check-plugin-teardown-shape --self-test (exit 3, control sha outside the shallow clone, fetched). node scripts/check-sdui-manifest.mjs exit 0.
  • pnpm --dir ui run check:new-line-citations exit 0 (0 new); eslint on the three touched plugin-report files: 0 errors (react-refresh warnings only, same rule the renderer already carries); node scripts/check-nul-bytes.mjs exit 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran; repo-wide pnpm lint is CI-owned.)
  • 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: its generated artifacts are regenerated and committed.
  • 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

…drawn table (exportOptions)

Claude-Session: https://claude.ai/code/session_01JQwxbYfDeaywgrS5N1PZfg
Co-authored-by: Claude <noreply@anthropic.com>
…s liveness to live

Claude-Session: https://claude.ai/code/session_01JQwxbYfDeaywgrS5N1PZfg
Co-authored-by: Claude <noreply@anthropic.com>
…/types; regenerate liveness counts

Claude-Session: https://claude.ai/code/session_01JQwxbYfDeaywgrS5N1PZfg
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/report.json, packages/spec/liveness/state-counts/report.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/report.json, packages/spec/liveness/state-counts/report.md) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 147 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3346da26dfdae0b77da2ae31fda67bb5293c8950 → packageMentionDocs.

…headers are data values

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

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 858f0fe5c3 · 2026-10-28T05:00Z

席位意见:报表页导出的就是屏幕上画出的那张表(含服务器合计),不重新查询;导出按钮按基础对象的 export 权限显示,与列表工具栏同一判断。

  1. Shape: draft, base main; the body says Refs #25873 (item: the report page reads exportOptions), with no closing keyword, as the order requires (the drill half stays Blocked-by analytics: a dataset query with a timeDimensions window returns no drill bounds for it, so a report drill-down lists records outside the picked period #27074). 9 files, about +720/−25. Not governed (check-governed-merges --pr 27089). ≤ 3000 lines.
  2. Premise measured. Seat read at head:
    • ReportExportControl calls all its hooks before its early returns. It draws nothing for formats: [], for no snapshot, or when the viewer's getObjectApiOperations(snapshot.object) lacks export. With no permissions provider, usePermissions returns the no-provider set, so the control shows, as on the list.
    • The table and the matrix publish their drawn cells through ReportTableSnapshotContext, keyed on content. Nothing re-queries or re-aggregates, so the file carries the table's own runtimeFilter and period, and the server totals.
    • A joined report gets no provider value and draws no control.
    • ExcelJS is imported only on an XLSX request; the same exceljs@4.4.0 is already a plugin-grid dependency.
    • Liveness: exportOptions and its three children flipped to live, with renderer evidence.
  3. Seat finding fixed at 858f0fe5. At 2809f49e, a matrix's across-axis headers (dimension VALUES via bucketLabel) reached the CSV header row without the formula guard. Every header cell now takes the guard, and a new matrix test pins =HYPERLINK("x") as tab-prefixed. The dev's ablation of that line turns exactly the new test red. XLSX was not affected, because ExcelJS stores strings as text. The seat applied the dev's body amendments (Tests 12/12, Verification head and counts, Files bullet) through the relay.
  4. Gate check-runs (pinned to head 858f0fe5c3). In progress at this record; landing waits for every check green. The dev's local reading: plugin-report 29 files, 269 passed / 7 skipped; lint 110/110; 83 derived gate families, 82 exit 0, and 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3). That gate scans backend packages/, which this diff does not change, so CI is the reading.
  5. Accepted deviations.
  6. Serial and landing.
    • Serial with PR feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070. Both edit DatasetReportRenderer.tsx, liveness/report.json, state-counts/report.md and the same lint block. Whichever lands second merges main, and its lint block must then pin both keys silent and pick another still-planned key as the warned control.
    • Landing: not governed. When every check on the current head is green, the seat lands it (pr_ready + automerge_enable), or asks the maintainer to click if the classifier refuses.

…-p301jn

# Conflicts:
#	packages/lint/src/lint-liveness-properties.test.ts
#	ui/packages/plugin-report/src/DatasetReportRenderer.tsx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 24357671e1 · 2026-10-28T09:47Z · re-pins ACCEPT 6461079012 (858f0fe5c3)

席位意见:#27070 合并后按约定合入 main,导出代码与已审版本一致,只把日期芯片和导出按钮并到同一行表头,lint 测试改为两个键都静默并带有仍会告警的对照项。

  1. Delta reviewed: 858f0fe5..24357671 is the merge 26b58f01 of origin/main 3346da26 (through os-regen-merge.sh, no rebase or force) plus the state-counts regen 24357671. Against 858f0fe5, the changeset, the export test file and ReportExport.tsx are byte-identical, except that one wrapper class changes from flex justify-end to ml-auto flex. package.json differs only by main's devDependency bumps. In DatasetReportRenderer.tsx, the period chip from feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 and the Export control now sit in one header row (flex flex-wrap items-center justify-between gap-2 empty:hidden), so a report with neither control draws no empty row. The period pending/error branch is kept as feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 wrote it.
  2. Lint pin: the #26548 block now asserts silence on both dateRange and exportOptions. Each row carries anti-vacuity reads: status: live, no authorWarn and no authorHint, and evidence citing ReportDateRange.tsx#reportTimeWindow and ReportExport.tsx#ReportExportControl. Both are authored in the same call as object.externalSharingModel, which is dead and must warn, as the warned control. report.md state counts read 27 live / 0 planned.
  3. Shape: unchanged. Refs #25873 (export slice), with no closing keyword. The drill half stays on the card, Blocked-by analytics: a dataset query with a timeDimensions window returns no drill bounds for it, so a report drill-down lists records outside the picked period #27074. The PR is not governed.
  4. Gate check-runs (pinned to head 24357671e1): 65 runs, 58 success and 7 skipped, none failing. The dev's local readings at this head: plugin-report type-check and tests exit 0 (30 files, 277 passed / 7 skipped), lint 110/110, check:liveness 0, dispatch-gates 83 derived / 83 run / 0 NOT MEASURED, and check-sdui-manifest 0.
  5. Accepted deviation: unchanged from 6461079012. The lint test edit is required by the ledger flip.
  6. Landing clause: the seat lands this head now, through the relay (pr_ready + automerge_enable), and tracks it to MERGED.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 09:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 09:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit c07f034 Oct 28, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-p301jn branch October 28, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants