fix(plugin-dashboard): 透视表 cell key 改用 JSON 编码,维度值含空格不再串格 (objectstack#5473) - #3414
Merged
Conversation
…ack#5473)
buildPivot keyed cellIndex on `${rowId} ${colId}` — a plain space — while
dimension values contain spaces constantly ("New York", "In Progress"). Two
rows whose ids met at a different point of the same string produced ONE key:
"New" x "York Q1" and "New York" x "Q1" both spelled `New York Q1`. The later
row overwrote the earlier one, so the cell showed another row's measure, the
overwritten row's value was unreachable, and drill-through read drillRawRows
by the same wrong index — all with no error.
Row ids and cell keys now go through pivotRowId / pivotCellKey, which encode
with JSON.stringify: the boundary is carried by JSON's own quoting rather than
by a character the data is assumed never to contain. The row id previously
relied on such a character (U+0001); keeping two encodings in one function
would have left the next reader to guess which applied where.
The row-subtotal lookup keys the same row buckets and now shares that encoder.
It hand-rolled its own join, which agreed with the row headers only while a
pivot had exactly one row dimension, so the Total column rendered blank for
every pivot with three or more dimensions.
Tests pin behaviour rather than key spelling — which (row, column) pair
resolves to which flat row index, resolved through the module's own key
builder. The previous cases spelled keys literally (`cellIndex.get('Open
High')`), which is why a key that merged two rows on a space read as correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…ck#5666 The null-placeholder collision the new encoder deliberately does NOT solve is now a filed issue; name it so the next reader does not re-derive it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 5, 2026 23:36
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 6, 2026
…uously (objectstack#5665) (objectstack-ai#3415) 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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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. Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes objectstack-ai/objectstack#5473
前提复核(对 origin/main 58a00f0)
空格拼接仍在,并已用探针复现串格。行维度
region、列维度quarter,两行:NewYork Q1New York Q1New YorkQ1New York Q1探针输出:
cellIndex只剩一个条目[ 'New York Q1', 1 ]。渲染出来是「New那一格显示 222」(另一行的度量值),111 在整张表里无处可达,drill-through 按同一个 index 钻到第 1 行的记录。全程无报错。改动
pivotCellKey(rowId, colId) = JSON.stringify([rowId, colId]),边界由 JSON 自己的引号/转义承担,不再依赖「数据不会包含的字符」。pivotRowId = JSON.stringify(values))。同函数里保留两种编码(cell key 用 JSON、row id 用 U+0001)是下一个读者的坑,而且 U+0001 那条仍然是「假设值里不会有这个字符」;换掉之后全函数只有一种编码策略。rowTotalById查的就是rowHeaders[].id,row id 编码一变,它必须跟着变,否则查不中。顺带说明第 3 点暴露的既有缺陷(改动前就存在,不是本次引入):它原先手写
join(''),而表头用的是join(U+0001)—— 只有「恰好一个行维度」时两者才碰巧相等,而仓库里每一条 pivot 用例都正好是这个形状。于是 3 个及以上维度的透视表,Total 列一直是空的。统一 encoder 后自然修复,并补了用例钉住。测试:钉行为,不钉编码
既有用例直接钉 key 字面形态(
cellIndex.get('Open High'))—— 这正是「一个把两行合并的 key」能长期读起来像对的原因。整条重写为cellAt(p, 行标签, 列标签):用模块自己的pivotCellKey按渲染器的路径解析,断言的是「哪个(行,列)组合落到哪个扁平行下标」,编码从此可以自由演进,保证不变。新增/重写:碰撞对不再串格(issue 的 New / York Q1 例)、多行维度且值含空格的同类碰撞、渲染 + drill-through 命中正确记录、多行维度的行小计。#3389 那条(objectstack#5450 补的多行维度用例)保留 fixture、去掉编码断言 —— 它钉的东西改为「两个行维度不会合并」本身。该文件不再以任何形式(字面量/转义/fromCharCode)引用控制字符。
反向验证(方向先声明,再运行)
把三处 encoder 还原成改前形态后,预测 4 红 2 绿,实际完全一致:
expected 1 to be 2—— 碰撞对合并成一个 cell(两条)expected [ 'New', '222', '—' ] to deeply equal [ 'New', '111', '—' ]—— 正是 issue 描述的症状expected [ '—', '—' ] to deeply equal [ '10', '20' ]—— 多行维度行小计验证
npx vitest run --maxWorkers=2 packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx(仓根跑,按文件名确认):47 passedtsc --noEmit:干净;lint:0 errorspnpm check:control-bytes:OK,另按文件自查[\x00-\x08\x0b\x0c\x0e-\x1f]无命中越界未改(已另开单)
packages/plugin-report/src/DatasetReportRenderer.tsx是同一家族的另一处:bucketId用空串 join 多个维度值(连边界都没有),cell key 同样是空格拼接。属另一个 widget,本 PR 不碰,单独开单。Generated by Claude Code