Skip to content

Commit 488cdae

Browse files
committed
feat: table sheet undo redo plugin
1 parent 6fd3c37 commit 488cdae

34 files changed

+2926
-189
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
coverage
3+
dist
4+
es
5+
cjs
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createFormulaAdapter, enhanceCellDataWithFormula } from '../../src/auto-fill/formula-integration';
2+
3+
function createTableStub(initial: Record<string, any> = {}) {
4+
const store = new Map<string, any>(Object.entries(initial));
5+
return {
6+
getCellValue: (col: number, row: number) => store.get(`${col}:${row}`),
7+
changeCellValue: (col: number, row: number, value: any) => {
8+
store.set(`${col}:${row}`, value);
9+
}
10+
} as any;
11+
}
12+
13+
test('DefaultFormulaAdapter detects and reads formula string', () => {
14+
const table = createTableStub({ '0:0': '=B1' });
15+
const adapter = createFormulaAdapter(table);
16+
expect(adapter.isFormulaCell(0, 0)).toBe(true);
17+
expect(adapter.getCellFormula(0, 0)).toBe('=B1');
18+
});
19+
20+
test('DefaultFormulaAdapter setCellFormula writes to table value', () => {
21+
const table = createTableStub();
22+
const adapter = createFormulaAdapter(table);
23+
adapter.setCellFormula(1, 2, '=A1+1');
24+
expect(table.getCellValue(1, 2)).toBe('=A1+1');
25+
});
26+
27+
test('CustomFormulaAdapter uses custom functions when provided', () => {
28+
const table = createTableStub({ '0:0': { f: 'SUM(A1:A2)' } });
29+
const adapter = createFormulaAdapter(
30+
table,
31+
(_c, _r, cellData) => Boolean(cellData?.f),
32+
(_c, _r, cellData) => (cellData?.f ? `=${cellData.f}` : undefined),
33+
(c, r, formula) => table.changeCellValue(c, r, { f: formula.replace(/^=/, '') })
34+
);
35+
36+
expect(adapter.isFormulaCell(0, 0)).toBe(true);
37+
expect(adapter.getCellFormula(0, 0)).toBe('=SUM(A1:A2)');
38+
39+
adapter.setCellFormula(0, 1, '=A1+1');
40+
expect(table.getCellValue(0, 1)).toEqual({ f: 'A1+1' });
41+
});
42+
43+
test('enhanceCellDataWithFormula attaches formula metadata for formula cells', () => {
44+
const table = createTableStub({ '0:0': '=B1' });
45+
const adapter = createFormulaAdapter(table);
46+
const cellData = { value: '=B1', col: 0, row: 0 } as any;
47+
const enhanced = enhanceCellDataWithFormula(cellData, 0, 0, adapter);
48+
expect(enhanced.isFormula).toBe(true);
49+
expect(enhanced.formula).toBe('=B1');
50+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
export {};
2+
3+
const CHANGE_CELL_VALUE = 'change_cell_value';
4+
const CHANGE_CELL_VALUES = 'change_cell_values';
5+
6+
jest.mock('@visactor/vtable', () => ({
7+
TABLE_EVENT_TYPE: {
8+
BEFORE_KEYDOWN: 'before_keydown',
9+
CHANGE_CELL_VALUE,
10+
CHANGE_CELL_VALUES,
11+
PASTED_DATA: 'pasted_data',
12+
ADD_RECORD: 'add_record',
13+
DELETE_RECORD: 'delete_record',
14+
UPDATE_RECORD: 'update_record',
15+
ADD_COLUMN: 'add_column',
16+
DELETE_COLUMN: 'delete_column',
17+
CHANGE_HEADER_POSITION: 'change_header_position',
18+
RESIZE_ROW: 'resize_row',
19+
RESIZE_ROW_END: 'resize_row_end',
20+
RESIZE_COLUMN: 'resize_column',
21+
RESIZE_COLUMN_END: 'resize_column_end'
22+
}
23+
}));
24+
25+
const { HistoryPlugin } = require('../src/history');
26+
27+
function createTestEnv() {
28+
const handlers: Record<string, Function[]> = {};
29+
const eventManager = {
30+
on: (type: string, cb: any) => {
31+
handlers[type] = handlers[type] || [];
32+
handlers[type].push(cb);
33+
},
34+
off: (type: string, cb: any) => {
35+
const list = handlers[type] || [];
36+
const idx = list.indexOf(cb);
37+
if (idx >= 0) {
38+
list.splice(idx, 1);
39+
}
40+
},
41+
emit: (type: string, event: any) => {
42+
(handlers[type] || []).forEach(cb => cb(event));
43+
}
44+
};
45+
46+
const formulaStore = new Map<string, string>();
47+
const formulaManager = {
48+
getCellFormula: (cell: any) => {
49+
return formulaStore.get(`${cell.sheet}:${cell.row}:${cell.col}`);
50+
},
51+
setCellContent: (cell: any, value: any) => {
52+
const key = `${cell.sheet}:${cell.row}:${cell.col}`;
53+
if (typeof value === 'string' && value.startsWith('=')) {
54+
formulaStore.set(key, value);
55+
} else {
56+
formulaStore.delete(key);
57+
}
58+
},
59+
getCellValue: (_cell: any) => {
60+
return { value: 28, error: undefined as string | undefined };
61+
},
62+
getCellDependents: (): any[] => []
63+
};
64+
65+
const worksheet = {
66+
eventManager,
67+
tableInstance: null as any
68+
};
69+
70+
const vtableSheet = {
71+
formulaManager,
72+
getWorkSheetByKey: (_sheetKey: string) => worksheet,
73+
workSheetInstances: new Map([['sheet1', worksheet]])
74+
};
75+
76+
const changeCellValueCalls: any[] = [];
77+
const table: any = {
78+
__vtableSheet: vtableSheet,
79+
options: {},
80+
records: [],
81+
editorManager: { editingEditor: null },
82+
changeCellValue: (...args: any[]) => {
83+
changeCellValueCalls.push(args);
84+
}
85+
};
86+
worksheet.tableInstance = table;
87+
88+
return { eventManager, formulaManager, table, vtableSheet, changeCellValueCalls };
89+
}
90+
91+
test('HistoryPlugin undo twice restores blank after two formula edits', () => {
92+
const env = createTestEnv();
93+
const plugin = new HistoryPlugin();
94+
95+
plugin.run({ row: 4, col: 1, currentValue: '' } as any, CHANGE_CELL_VALUE as any, env.table as any);
96+
env.formulaManager.setCellContent({ sheet: 'sheet1', row: 4, col: 1 }, '=sum(B4)');
97+
env.eventManager.emit('formula_added', { cell: { row: 4, col: 1 }, formula: '=sum(B4)' });
98+
99+
plugin.run(
100+
{
101+
values: [{ row: 4, col: 1, currentValue: '', changedValue: 28 }]
102+
} as any,
103+
CHANGE_CELL_VALUES as any,
104+
env.table as any
105+
);
106+
107+
plugin.run({ row: 4, col: 1, currentValue: 28 } as any, CHANGE_CELL_VALUE as any, env.table as any);
108+
env.formulaManager.setCellContent({ sheet: 'sheet1', row: 4, col: 1 }, '=sum(B3:B4)');
109+
env.eventManager.emit('formula_added', { cell: { row: 4, col: 1 }, formula: '=sum(B3:B4)' });
110+
111+
plugin.undo();
112+
expect(env.formulaManager.getCellFormula({ sheet: 'sheet1', row: 4, col: 1 })).toBe('=sum(B4)');
113+
114+
plugin.undo();
115+
expect(env.formulaManager.getCellFormula({ sheet: 'sheet1', row: 4, col: 1 })).toBeUndefined();
116+
});

0 commit comments

Comments
 (0)