Skip to content

fix(vtable): map sorted view index to source record in deleteRecords/updateRecords - #5330

Open
william-xue wants to merge 3 commits into
VisActor:developfrom
william-xue:fix/sorted-sync-record-index
Open

william-xue wants to merge 3 commits into
VisActor:developfrom
william-xue:fix/sorted-sync-record-index

Conversation

@william-xue

Copy link
Copy Markdown

[中文版模板 / Chinese template]

🤔 This is a ...

  • New feature
  • Bug fix
  • TypeScript definition update
  • Bundle size optimization
  • Performance optimization
  • Enhancement feature
  • Refactoring
  • Update dependency
  • Code style optimization
  • Test Case
  • Branch merge
  • Site / documentation update
  • Demo update
  • Workflow
  • Chore
  • Release
  • Other (about what?)

🔗 Related issue link

No tracking issue. Found while checking syncRecordOperationsToSourceRecords on a sorted table.

💡 Background and solution

syncRecordOperationsToSourceRecords: true promises that deleteRecords / updateRecords write back to the array passed in options.records. Both APIs receive body display indices, and the sync branches in DataSource used that display index directly as an index into this.records:

// deleteRecords(recordIndexs, true)
const deletedRecord = this.records[viewIndex];   // viewIndex is a display index

// updateRecords(records, recordIndexs, true)
const oldRecord = this.records[recordIndex];     // recordIndex is a display index

this.records is _source, i.e. the data source array in source order. Sorting does not reorder _source: DataSource.sort() only stores the view → source mapping in currentIndexedData, which is exactly what the non-sync paths already use (deleteRecordsForSorted / updateRecordsForSorted both read this.currentIndexedData[recordIndex]). Filtering keeps the same contract, because the view index then maps identically onto the filtered _source.

So on a sorted table the sync path touched the wrong record. Reproduced with the published 1.26.8 UMD bundle (desc-sorted id, 5 records, headless Chrome):

operation (view [5,4,3,2,1]) before after
deleteRecords([0]) source [2,3,4,5], view [5,4,3,2] — id 5 still visible, id 1 silently dropped source [1,2,3,4], view [4,3,2,1]
updateRecords([{id:5,name:'FIVE-UPDATED'}], [0]) source [0] (id 1) replaced, view [5,5,4,3,2] — duplicate id 5 source id 5 replaced, view [5,4,3,2,1]

Fix: map the display index back to the data source index through currentIndexedData before reading this.records (_getRecordIndexFromViewIndex, identity when the table is neither sorted nor filtered, so the existing behavior is unchanged). Tree / grouped data are untouched — those entries are arrays and keep the previous fallback.

📝 Changelog

Language Changelog
🇺🇸 English fix(vtable): map sorted view index to the source record in deleteRecords/updateRecords
🇨🇳 Chinese 修复(vtable):排序状态下 deleteRecords/updateRecords 同步数据源时按视图索引映射到正确记录

☑️ Self-Check before Merge

⚠️ Please check all items below before requesting a reviewing. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • TypeScript definition is updated/provided or not needed
  • Changelog is provided or not needed

🚀 Summary

copilot:summary

🔍 Walkthrough

copilot:walkthrough

…updateRecords

deleteRecords/updateRecords receive body display indices, but the sync
branches of DataSource used them directly as indices into this.records
(_source, source order). Sorting keeps _source in source order and stores
the view -> source mapping in currentIndexedData, so on a sorted table the
wrong record was deleted/updated in the user's records array.
@fangsmile

fangsmile commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

有 2 处阻塞性问题需要修正后再合并:

  1. 排序表批量删除时,第一次 rawRecords.splice() 会使后续映射失效。在无筛选场景下 rawRecords 与 this.records 是同一数组,但循环每次仍使用删除前的 currentIndexedData。例如源数据 [1,2,3,4,5] 降序显示,删除视图行 [0,1] 时会先按映射删除 id 4;数组缩短后,视图行 0 对应的旧源下标 4 已越界,id 5 会漏删,但方法仍把两个视图索引都报告为已删除。应在修改数组前一次性解析待删记录/原始下标,再按原始下标降序删除,并补排序状态下批量删除测试。

  2. 分页非首页时映射到了错误记录。deleteRecords / updateRecords 接收当前页 body 显示索引,但 _getRecordIndexFromViewIndex() 使用全量 currentIndexedData[viewIndex],没有分页偏移。例如第二页显示行 0 应通过 currentPagerIndexedData[0] 定位,当前却会操作全量排序结果的第 0 条,即其他页面的记录。建议复用 getRecordIndexPaths(viewIndex) / currentPagerIndexedData,并补 currentPage > 0 下排序表删除、更新测试。

- resolve every body view index to its source record first, then delete the
  raw indexes in descending order, so a batch delete on a sorted table no
  longer misses records when rawRecords and this.records are the same array
- map the body view index through _currentPagerIndexedData (same as
  getRecordIndexPaths) so deleteRecords/updateRecords on currentPage > 0
  operate on the records shown on the current page
- add tests for sorted batch delete, sorted update and paginated delete/update
@william-xue

Copy link
Copy Markdown
Author

已按这两点提交 df4a503:

  1. 批量删除:改为先遍历一遍把 body 显示索引解析成「记录 → 原始下标」,收集完再去重、按原始下标降序 splice,避免 rawRecords 与 this.records 为同一数组时后续映射失效。降序 [5,4,3,2,1] 删除视图行 [0,1] 现在会确实删掉 id 5 与 id 4。
  2. 分页:_getRecordIndexFromViewIndex 改用 _currentPagerIndexedData(即 getRecordIndexPaths(viewIndex))取当前页映射,不再用全量 currentIndexedData;deleteRecords/updateRecords 的显示索引边界也改为按当前页行数判断,所以 currentPage > 0 时不会再落到其他页的记录上。

新增 4 个用例:排序表单条/批量删除、排序表更新、分页排序表(perPageCount: 2, currentPage: 1)下的删除与更新。本地仓库没有 node_modules 跑不了 jest-electron,单测交由 CI(unit-test)执行。

@visactor-review-bot visactor-review-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest reviewed commit: cdab8fe

No confirmed new issues found.

if (rawIndex >= 0) {
rawRecords.splice(rawIndex, 1);
rawDeletedIndexs.push(rawIndex);
pendingDeletes.push(rawIndex);

@visactor-review-bot visactor-review-bot Bot Oct 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 批量删除重复引用记录时避免重复删除同一原始下标

当 syncRecordOperationsToSourceRecords: true 且源数组中复用同一记录对象时,不同显示行经 rawRecords.indexOf(deletedRecord) 会解析到同一原始下标;这里将它多次加入 pendingDeletes,后面连续 splice 会误删其他行。例如无排序、无筛选时源数组为 [A, B, A, B](两个 A 引用同一对象,两个 B 亦然),调用 deleteRecords([1, 2, 3]) 应留下 A,但待删下标变成 [1, 1, 0],实际留下 B;改动前逐次删除在此输入会留下 A。这与既有讨论中的排序后映射失效不同,是预收集下标带来的新回归。建议直接使用已映射的源下标(筛选态则建立按出现次数匹配的原始位置映射),确保每个显示行对应不同原始槽位。上述结果来自按源码删除循环构造的 Node 最小复现,未运行工程测试。


Confirmed fixed (cdab8fe): For the original [A, B, A, B] input deleting view rows [1, 2, 3], the reservedRawIndexs loop now skips already-reserved source positions, then splices distinct positions in descending order. The ListTable sync path still invokes this branch for non-tree, non-grouped tables (record-helper.ts:1004-1009). A source-matched Node reproduction leaves A, so the original incorrect result is gone; this is not a full integration test.

deleteRecords 的同步分支先把显示行映射成「待删记录」,再用 rawRecords.indexOf
得到原始下标并按降序 splice。当同一个记录对象在源数组中被引用多次时(例如多张
表共用同一条记录),indexOf 会对多条显示行返回同一个原始下标,这个下标被重复
加入待删列表后连续 splice,会连带删掉别的行:源数组 [A, B, A, B] 删除显示行
[1, 2, 3] 时留下的是 B 而不是 A。

改为在解析阶段为每条显示行预留一个尚未占用的原始槽位(命中已占用的下标时,
indexOf 从该位置继续向后查找),保证显示行与原始槽位一一对应。记录互不重复时
行为不变。

新增 1 条回归用例:源数组引用同一对象两次,批量删除 [1, 2, 3] 后只保留第 0 行
引用的记录。
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.

2 participants