Skip to content

Commit 3053821

Browse files
committed
feat: filter plugin operate undo redo
1 parent 95f45b8 commit 3053821

20 files changed

+1360
-56
lines changed

packages/vtable-plugins/src/filter/filter-state-manager.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ListTable, PivotTable } from '@visactor/vtable';
2-
import type { FilterState, FilterAction, FilterConfig, FilterListener } from './types';
2+
import type { FilterState, FilterAction, FilterConfig, FilterListener, FilterStateSnapshot } from './types';
33
import { FilterActionType } from './types';
44
import type { FilterEngine } from './filter-engine';
55

@@ -45,6 +45,145 @@ export class FilterStateManager {
4545
return this.state;
4646
}
4747

48+
/**
49+
* 生成可序列化的筛选快照,用于撤销/恢复或跨实例回放。
50+
* - 只保留声明式配置(byValue 的 values、byCondition 的 operator/condition 等),不包含运行时函数。
51+
* - 对 byValue 的 values 做排序,避免因为用户勾选顺序不同导致快照 diff 抖动。
52+
* - 对 filters 按 field 排序,保证快照稳定。
53+
*/
54+
getSnapshot(): FilterStateSnapshot {
55+
const filters: FilterConfig[] = [];
56+
this.state.filters.forEach(v => {
57+
const next: any = { ...v };
58+
if (next && next.type === 'byValue' && Array.isArray(next.values)) {
59+
next.values = next.values.slice().sort((a: any, b: any) => String(a).localeCompare(String(b)));
60+
}
61+
if (next && Array.isArray(next.condition) && next.condition.length > 2) {
62+
next.condition = next.condition.slice();
63+
}
64+
filters.push(next);
65+
});
66+
filters.sort((a, b) => String(a.field).localeCompare(String(b.field)));
67+
return { filters };
68+
}
69+
70+
/**
71+
* 从快照恢复筛选状态并立即重新应用筛选。
72+
* 典型用途:HistoryPlugin 回放(undo/redo)时恢复筛选配置。
73+
*/
74+
applySnapshot(snapshot: FilterStateSnapshot, actionType: FilterActionType = FilterActionType.APPLY_FILTERS): void {
75+
const next = new Map<string | number, FilterConfig>();
76+
(snapshot?.filters ?? []).forEach(cfg => {
77+
if (!cfg) {
78+
return;
79+
}
80+
const cloned: any = { ...cfg };
81+
if (cloned && cloned.type === 'byValue' && Array.isArray(cloned.values)) {
82+
cloned.values = cloned.values.slice();
83+
}
84+
if (cloned && Array.isArray(cloned.condition)) {
85+
cloned.condition = cloned.condition.slice();
86+
}
87+
next.set(cloned.field, cloned);
88+
});
89+
this.state = { ...this.state, filters: next };
90+
this.applyFilters();
91+
this.notifyListeners({ type: actionType, payload: { fromSnapshot: true } } as any);
92+
}
93+
94+
/**
95+
* 列插入后修正筛选配置中的 field。
96+
* 背景:ListTable.addColumns(..., isMaintainArrayData=true) 会重排 columns[i].field=i,
97+
* 此时 FilterStateManager 里如果存的是数字 field(数组 records 场景),需要同步 +columnCount,
98+
* 否则旧筛选会错位应用到新列,表现为“全部被过滤掉/筛选异常”。
99+
*/
100+
shiftFieldsOnAddColumns(columnIndex: number, columnCount: number): void {
101+
if (!Number.isFinite(columnIndex) || !Number.isFinite(columnCount) || columnCount <= 0) {
102+
return;
103+
}
104+
const next = new Map<string | number, FilterConfig>();
105+
this.state.filters.forEach((cfg, key) => {
106+
let newKey: string | number = key;
107+
const cloned: any = { ...cfg };
108+
if (typeof newKey === 'number' && newKey >= columnIndex) {
109+
newKey = newKey + columnCount;
110+
}
111+
if (typeof cloned.field === 'number' && cloned.field >= columnIndex) {
112+
cloned.field = cloned.field + columnCount;
113+
}
114+
if (cloned && cloned.type === 'byValue' && Array.isArray(cloned.values)) {
115+
cloned.values = cloned.values.slice();
116+
}
117+
if (cloned && Array.isArray(cloned.condition)) {
118+
cloned.condition = cloned.condition.slice();
119+
}
120+
next.set(newKey, cloned);
121+
});
122+
this.state = { ...this.state, filters: next };
123+
}
124+
125+
/**
126+
* 列删除后修正筛选配置中的 field。
127+
* - 命中被删除列的筛选项会被移除(否则将引用无效列)。
128+
* - 删除多列时按升序依次“前移”,与 ListTable.deleteColumns 的维护逻辑一致。
129+
*/
130+
shiftFieldsOnDeleteColumns(deleteColIndexs: number[]): void {
131+
if (!Array.isArray(deleteColIndexs) || deleteColIndexs.length === 0) {
132+
return;
133+
}
134+
const sorted = deleteColIndexs
135+
.slice()
136+
.filter(n => Number.isFinite(n))
137+
.sort((a, b) => a - b);
138+
const deleteIndexNums = sorted.map((idx, i) => idx - i);
139+
140+
const next = new Map<string | number, FilterConfig>();
141+
this.state.filters.forEach((cfg, key) => {
142+
let newKey: string | number = key;
143+
const cloned: any = { ...cfg };
144+
let removed = false;
145+
if (typeof newKey === 'number') {
146+
let k = newKey;
147+
for (let i = 0; i < deleteIndexNums.length; i++) {
148+
const d = deleteIndexNums[i];
149+
if (k === d) {
150+
removed = true;
151+
break;
152+
}
153+
if (k > d) {
154+
k -= 1;
155+
}
156+
}
157+
newKey = k;
158+
}
159+
if (typeof cloned.field === 'number') {
160+
let f = cloned.field;
161+
for (let i = 0; i < deleteIndexNums.length; i++) {
162+
const d = deleteIndexNums[i];
163+
if (f === d) {
164+
removed = true;
165+
break;
166+
}
167+
if (f > d) {
168+
f -= 1;
169+
}
170+
}
171+
cloned.field = f;
172+
}
173+
if (removed) {
174+
return;
175+
}
176+
if (cloned && cloned.type === 'byValue' && Array.isArray(cloned.values)) {
177+
cloned.values = cloned.values.slice();
178+
}
179+
if (cloned && Array.isArray(cloned.condition)) {
180+
cloned.condition = cloned.condition.slice();
181+
}
182+
next.set(newKey, cloned);
183+
});
184+
this.state = { ...this.state, filters: next };
185+
}
186+
48187
/**
49188
* 公共方法:重新应用当前所有激活的筛选状态
50189
* 用于在表格配置更新后恢复筛选显示

packages/vtable-plugins/src/filter/filter-toolbar.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ export class FilterToolbar {
3030
private clearFilterOptionLink: HTMLAnchorElement;
3131
private cancelFilterButton: HTMLButtonElement;
3232
private applyFilterButton: HTMLButtonElement;
33+
private pluginId: string;
3334

3435
constructor(table: ListTable | PivotTable, filterStateManager: FilterStateManager, pluginOptions: FilterOptions) {
3536
this.table = table;
3637
this.filterStateManager = filterStateManager;
3738
this.valueFilter = new ValueFilter(this.table, this.filterStateManager, pluginOptions);
3839
this.conditionFilter = new ConditionFilter(this.table, this.filterStateManager, pluginOptions, this.hide);
3940
this.pluginOptions = pluginOptions;
41+
this.pluginId = pluginOptions?.id ?? 'filter';
4042

4143
this.filterMenuWidth = 300; // 待优化,可能需要自适应内容的宽度
4244

@@ -93,6 +95,26 @@ export class FilterToolbar {
9395
this.hide();
9496
}
9597

98+
private recordHistory(before: any, after: any): void {
99+
const beforeKey = JSON.stringify(before);
100+
const afterKey = JSON.stringify(after);
101+
if (beforeKey === afterKey) {
102+
return;
103+
}
104+
const pm: any = (this.table as any).pluginManager;
105+
const history =
106+
pm?.getPlugin?.('history-plugin') ??
107+
pm?.getPluginByName?.('History') ??
108+
pm?.getPlugin?.('history') ??
109+
pm?.getPluginByName?.('history');
110+
history?.recordExternalCommand?.({
111+
type: 'filter',
112+
pluginId: this.pluginId,
113+
oldSnapshot: before,
114+
newSnapshot: after
115+
});
116+
}
117+
96118
/**
97119
* 更新清除筛选按钮的状态
98120
*/
@@ -198,11 +220,17 @@ export class FilterToolbar {
198220

199221
this.clearFilterOptionLink.addEventListener('click', e => {
200222
e.preventDefault();
223+
const before = this.filterStateManager.getSnapshot();
201224
this.clearFilter(this.selectedField);
225+
const after = this.filterStateManager.getSnapshot();
226+
this.recordHistory(before, after);
202227
});
203228

204229
this.applyFilterButton.addEventListener('click', () => {
230+
const before = this.filterStateManager.getSnapshot();
205231
this.applyFilter(this.selectedField);
232+
const after = this.filterStateManager.getSnapshot();
233+
this.recordHistory(before, after);
206234
});
207235

208236
// 点击空白处整个筛选菜单可消失

packages/vtable-plugins/src/filter/filter.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { TABLE_EVENT_TYPE, TYPES } from '@visactor/vtable';
22
import { FilterEngine } from './filter-engine';
33
import { FilterStateManager } from './filter-state-manager';
44
import { FilterToolbar } from './filter-toolbar';
5-
import type { FilterOptions, FilterConfig, FilterState, FilterAction } from './types';
5+
import type { FilterOptions, FilterConfig, FilterState, FilterAction, FilterStateSnapshot } from './types';
66
import { FilterActionType } from './types';
77
import type {
88
ListTableConstructorOptions,
@@ -31,7 +31,9 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin {
3131
TABLE_EVENT_TYPE.CHANGE_CELL_VALUE,
3232
TABLE_EVENT_TYPE.UPDATE_RECORD,
3333
TABLE_EVENT_TYPE.ADD_RECORD,
34-
TABLE_EVENT_TYPE.DELETE_RECORD
34+
TABLE_EVENT_TYPE.DELETE_RECORD,
35+
TABLE_EVENT_TYPE.ADD_COLUMN,
36+
TABLE_EVENT_TYPE.DELETE_COLUMN
3537
];
3638

3739
pluginOptions: FilterOptions;
@@ -138,9 +140,33 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin {
138140
} else if (runtime === TABLE_EVENT_TYPE.UPDATE_RECORD) {
139141
this.syncFilterWithTableData();
140142
} else if (runtime === TABLE_EVENT_TYPE.ADD_RECORD) {
143+
// #region 为了解决“ 已处于筛选状态时插入新行(尤其是空数组 [] 这种草稿行),后续再点一次筛选确认/切换其他列筛选后,这条新行会莫名其妙消失,甚至导致看起来全被过滤掉 ”的问题。
144+
// 解决思路:
145+
// 当触发 ADD_RECORD 事件且当前已经有激活筛选时
146+
// 把这次新增的 record(s) 逐个 markForceVisibleRecord
147+
// 让它们在后续 updateFilterRules 重新筛选时不会立刻被刷掉,从而“草稿行可见、可继续编辑”
148+
const hasActiveFilter = this.filterStateManager?.getActiveFilterFields?.().length > 0;
149+
if (hasActiveFilter && Array.isArray(eventArgs?.records)) {
150+
const ds: any = (this.table as any).dataSource;
151+
eventArgs.records.forEach((r: any) => ds?.markForceVisibleRecord?.(r));
152+
}
153+
// #endregion
141154
this.syncFilterWithTableData();
142155
} else if (runtime === TABLE_EVENT_TYPE.DELETE_RECORD) {
143156
this.syncFilterWithTableData();
157+
} else if (runtime === TABLE_EVENT_TYPE.ADD_COLUMN) {
158+
const columnIndex = eventArgs?.columnIndex;
159+
const columnCount = eventArgs?.columnCount;
160+
if (typeof columnIndex === 'number' && typeof columnCount === 'number' && columnCount > 0) {
161+
this.filterStateManager?.shiftFieldsOnAddColumns?.(columnIndex, columnCount);
162+
}
163+
this.reapplyActiveFilters();
164+
} else if (runtime === TABLE_EVENT_TYPE.DELETE_COLUMN) {
165+
const deleteColIndexs = eventArgs?.deleteColIndexs;
166+
if (Array.isArray(deleteColIndexs) && deleteColIndexs.length > 0) {
167+
this.filterStateManager?.shiftFieldsOnDeleteColumns?.(deleteColIndexs);
168+
}
169+
this.reapplyActiveFilters();
144170
}
145171
}
146172

@@ -155,6 +181,14 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin {
155181
});
156182
}
157183

184+
getFilterSnapshot(): FilterStateSnapshot {
185+
return this.filterStateManager?.getSnapshot?.() ?? { filters: [] };
186+
}
187+
188+
applyFilterSnapshot(snapshot: FilterStateSnapshot): void {
189+
this.filterStateManager?.applySnapshot?.(snapshot, FilterActionType.APPLY_FILTERS);
190+
}
191+
158192
// 当用户的配置项更新时调用
159193
update() {
160194
if (this.filterStateManager) {

packages/vtable-plugins/src/filter/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface FilterState {
3939
// activeFilters: string[]; // 激活的筛选器的 ID 列表
4040
}
4141

42+
export interface FilterStateSnapshot {
43+
filters: FilterConfig[];
44+
}
45+
4246
export interface FilterConfig {
4347
enable: boolean; // 是否启用筛选
4448
field: string | number; // 对应表格列,同时作为筛选配置的唯一标识

0 commit comments

Comments
 (0)