Skip to content

fix(plugin-report): 透视表 bucket id / cell key 改用 JSON 编码,维度值不再串桶串格 (objectstack#5665) - #3415

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5665-report-pivot-encoding
Aug 6, 2026
Merged

fix(plugin-report): 透视表 bucket id / cell key 改用 JSON 编码,维度值不再串桶串格 (objectstack#5665)#3415
yinlianghui merged 1 commit into
mainfrom
claude/issue-5665-report-pivot-encoding

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5665

问题

packages/plugin-report/src/DatasetReportRenderer.tsx 的 matrix 交叉表把三类查找都建在「维度值拼接」上,两处拼接各自有缺陷:

  1. bucketId 用真空串 join('') —— 相邻维度值之间一个边界字符都没有bucketId(['region','segment'], …)x + yzxy + z 同样产出 xyz。这条同时作用在行轴和列轴上(rid / cid 都走它),两个不同的桶合并成一个,后写覆盖先写。
  2. cell key 用普通空格拼 rid/cid —— 与 fix(plugin-dashboard): 透视表 cell key 改用 JSON 编码,维度值含空格不再串格 (objectstack#5473) #3414 在 dashboard 修掉的完全同形。维度值含空格是常态(New York / In Progress),于是 New × York Q1New York × Q1 落进同一个 key。

合并后的桶显示的是另一行的度量,被覆盖那行的值取不到;cells 的 value 还带 index,而 drill-through 正是用它读 drillRawRows,所以点开的是另一条记录的列表。全程无任何报错。

修法(与 PR #3414 同形,整体换)

bucketId 改走 JSON.stringify:边界由 JSON 自身的引号承载,不再依赖「数据不会包含的字符」。

本渲染器有四处查找共用这一个 encoder —— 行表头、列表头、行小计 rowTotalById、列小计 colTotalById —— 所以整体换:任何一处留在旧拼法,就会把 #3414 刚在 dashboard 侧修掉的「表头与小计各用一套编码」重新引进来。cell key 的 setget 两处也一并换成 pivotCellKey

共享 helper(按 PM 裁定核了依赖拓扑)

plugin-reportplugin-dashboardworkspace:* 依赖 @object-ui/core,且已有先例 —— buildDatasetDrillFilter 正是为「两边 drill 行为一致」抽过去的。方向干净,因此:

  • 新增 packages/core/src/utils/dataset-pivot.ts,导出 pivotBucketId / pivotCellKey;
  • DatasetWidget.tsx 改 import(pivotBucketId as pivotRowId)+ 删本地 helper,导出面、调用点、行为一律不动,fix(plugin-dashboard): 透视表 cell key 改用 JSON 编码,维度值含空格不再串格 (objectstack#5473) #3414 的用例全部复跑通过;
  • 核心命名取 pivotBucketId 而非 pivotRowId:report 侧列轴也用它编码(dashboard 的 across 只有单维度,行轴才拼多值),叫 row 会在 cid 处误导。dashboard 侧用别名保留其原有本地名,不扩大改动面。

两个包各写一份同样的 key,正是同一个碰撞被发现两次、修两次的根因;现在只有一份实现。

用例(钉行为,不钉 key 拼法)

新增 5 例,覆盖 issue 与 PM 点名的每一族:

用例 修前 修后
行轴 x+yz vs xy+z 塌成一行,显示 2 两行,各显示 1 / 2
列轴 Q+1x vs Q1+x(含列小计) 塌成一列 两列,小计各归各位
空格碰撞 New × York Q1 vs New York × Q1 New 显示 222 显示 111,缺失格为
drill 命中 objectFilter = NEW-YORK / Q1(另一行) NEW / YORK-Q1
多维行小计匹配 只剩 1 个小计 ['1','2']

反向验证方向(修前红 / 修后绿,预先判定并复现):先只加用例、不动实现,5 例全红,且失败形态与预测逐条吻合 —— drill 那例直接打出 billing_state: "NEW-YORK",即「钻到另一行」的实证。同文件既有 36 例始终绿:它们的 pivot fixture 行/列各只有一个维度、值不含空格,此时空串分隔与正确分隔不可区分,所以两个缺陷从未被跑到。

消费半径清扫

  • grep 全仓已无 `${a} ${b}` 形态的 map key;.join('') 的其余命中都是图标名/首字母拼接,不是桶 id。
  • plugin-dashboard/PivotTable.tsx 用嵌套对象 bucket[r][c]、单行单列字段,不属此类,无需改动。

验证

  • vitest run packages/plugin-report/.../DatasetReportRenderer.test.tsx packages/plugin-dashboard/.../DatasetWidget.test.tsx88 passed (2 files)
  • type-check:@object-ui/core / plugin-report(含 tsconfig.test.json)/ plugin-dashboard 全部 Done
  • pnpm check:control-bytes OK,并对本 PR 全部改动文件做了越过 gate 的自扫(含 0x01 等 gate 不扫的字节),无命中

已知残留(不在本 PR 范围)

null/undefined 维度值仍编码为占位符 ,与字面等于该占位符的值相撞 —— 这是占位符的性质而非编码的,已由 objectstack#5666 单独跟踪,dataset-pivot.ts 的注释里指了过去。

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…uously (objectstack#5665)

The cross-tab's bucketId joined dimension values with the EMPTY string, so
adjacent values had no boundary at all: bucketId(['region','segment'], …)
spelled "xyz" for BOTH "x" + "yz" and "xy" + "z". Two different buckets became
one on either axis and the later row silently overwrote the earlier one. The
cell key then joined the two bucket ids with a plain space — the same defect
#3414 fixed in the dashboard widget — while dimension values carry spaces
constantly, so "New" x "York Q1" and "New York" x "Q1" met in one key too.

A merged bucket showed a different row's measure, the overwritten row's value
was unreachable, and because the cell entry carries the flat row index that
drill-through reads drillRawRows by, the click drilled into another record's
list. All of it silent.

bucketId now encodes with JSON.stringify, whose quoting carries the boundary
instead of a character the data is assumed never to contain. This renderer
keys FOUR lookups off the same encoder — row headers, column headers, the row
subtotal map and the column subtotal map — so the swap is wholesale; leaving
any one on the old spelling would reintroduce the header-vs-subtotal split
#3414 had just removed on the dashboard side.

The encoders moved to @object-ui/core (pivotBucketId / pivotCellKey) and are
now shared with plugin-dashboard, whose local copies they replace: both
packages hand-rolling the same key is the reason the same collision had to be
found and fixed twice. The dashboard's exports, call sites and behaviour are
unchanged — its #3414 cases still pass.

Tests assert behaviour, not key spelling: which bucket renders which measure,
which raw record a colliding cell drills to, and which subtotal lands under
which multi-dimension row. The pre-existing pivot cases use one row dimension
and space-free values, where an empty separator is indistinguishable from a
correct one — which is why they never saw either defect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 6, 2026 12:20am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-D4BLQn3B.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 478.45KB 105.05KB
core (index.js) 2.47KB 0.91KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 228.08KB 55.90KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.03KB 28.88KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 231.85KB 57.18KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.02KB 25.36KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 6, 2026 00:26
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 509104a Aug 6, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5665-report-pivot-encoding branch August 6, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: DatasetReportRenderer 的 pivot bucket id 用空串 join 维度值、cell key 用空格拼接 —— 与 #5473 同族,且更早合并

2 participants