snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RemoveSuperTableMutation, SetSuperTableMutation } from '@univerjs/engine-formula';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { SheetTableFormulaController } from '../sheet-table-formula.controller';
describe('SheetTableFormulaController', () => {
it('should sync super table metadata on add/range/name/delete events', () => {
const tableAdd$ = new Subject<any>();
const tableRangeChanged$ = new Subject<any>();
const tableDelete$ = new Subject<any>();
const tableNameChanged$ = new Subject<any>();
const table = {
getTableInfo: () => ({
name: 'Orders',
subUnitId: 's1',
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
columns: [
{ displayName: 'id' },
{ displayName: 'amount' },
],
}),
};
const tableManager = {
tableAdd$,
tableRangeChanged$,
tableDelete$,
tableNameChanged$,
getTableById: vi.fn(() => table),
};
const executeCommand = vi.fn();
const commandService = { executeCommand };
const controller = new SheetTableFormulaController(tableManager as any, commandService as any);
tableAdd$.next({ unitId: 'u1', tableId: 't1' });
expect(executeCommand).toHaveBeenCalledWith(SetSuperTableMutation.id, expect.objectContaining({
unitId: 'u1',
tableName: 'Orders',
reference: expect.objectContaining({
sheetId: 's1',
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
}),
}), { onlyLocal: true });
const titleMap = executeCommand.mock.calls[0][1].reference.titleMap as Map<string, number>;
expect(titleMap.get('id')).toBe(0);
expect(titleMap.get('amount')).toBe(1);
tableRangeChanged$.next({ unitId: 'u1', tableId: 't1' });
expect(executeCommand).toHaveBeenCalledTimes(2);
tableNameChanged$.next({ unitId: 'u1', tableId: 't1', oldTableName: 'Orders_Old' });
expect(executeCommand).toHaveBeenNthCalledWith(3, RemoveSuperTableMutation.id, {
unitId: 'u1',
tableName: 'Orders_Old',
}, { onlyLocal: true });
expect(executeCommand).toHaveBeenCalledTimes(4);
tableDelete$.next({
unitId: 'u1',
subUnitId: 's1',
tableName: 'Orders',
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
});
expect(executeCommand).toHaveBeenLastCalledWith(RemoveSuperTableMutation.id, {
unitId: 'u1',
tableName: 'Orders',
reference: {
sheetId: 's1',
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
},
}, { onlyLocal: true });
expect(executeCommand.mock.calls.every((call) => call[2]?.onlyLocal === true)).toBe(true);
controller.dispose();
});
});

View File

@@ -0,0 +1,850 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, ICellData, IWorkbookData, Nullable, UnitModel, Workbook } from '@univerjs/core';
import type { BaseAstNode, BaseFunction, IUnitData } from '@univerjs/engine-formula';
import type { IRender } from '@univerjs/engine-render';
import {
CellValueType,
ICommandService,
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
ObjectMatrix,
Plugin,
RANGE_TYPE,
RedoCommand,
set,
ThemeService,
UndoCommand,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import {
AstRootNodeFactory,
AstTreeBuilder,
CalculateFormulaService,
DefinedNamesService,
FormulaCurrentConfigService,
FormulaDataModel,
FormulaDependencyGenerator,
FormulaRuntimeService,
FormulaUnitReferenceResolver,
functionMath,
FunctionNodeFactory,
FunctionService,
generateExecuteAstNodeData,
getObjectValue,
HyperlinkEngineFormulaService,
ICalculateFormulaService,
IDefinedNamesService,
IFormulaCurrentConfigService,
IFormulaDependencyGenerator,
IFormulaExternalReferenceDataLoader,
IFormulaRuntimeService,
IFormulaUnitReferenceResolver,
IFunctionService,
IHyperlinkEngineFormulaService,
Interpreter,
IOtherFormulaManagerService,
ISheetRowFilteredService,
ISuperTableService,
LambdaNodeFactory,
LambdaParameterNodeFactory,
Lexer,
LexerNode,
LexerTreeBuilder,
NoopFormulaExternalReferenceDataLoader,
OperatorNodeFactory,
OtherFormulaManagerService,
PrefixNodeFactory,
ReferenceNodeFactory,
RemoveSuperTableMutation,
SetArrayFormulaDataMutation,
SetFormulaDataMutation,
SetSuperTableMutation,
SheetRowFilteredService,
SuffixNodeFactory,
SuperTableService,
UnionNodeFactory,
ValueNodeFactory,
} from '@univerjs/engine-formula';
import { Engine, IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
import { InsertColMutation, InsertRowMutation, MoveRangeCommand, MoveRangeMutation, RangeProtectionRuleModel, RefRangeService, RemoveColByRangeCommand, RemoveColCommand, RemoveColMutation, RemoveRowByRangeCommand, RemoveRowCommand, RemoveRowMutation, SetRangeValuesCommand, SetRangeValuesMutation, SheetInterceptorService, SheetSkeletonService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets';
import { UpdateFormulaController } from '@univerjs/sheets-formula';
import enUS from '@univerjs/sheets/locale/en-US';
import zhCN from '@univerjs/sheets/locale/zh-CN';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DeleteSheetTableCommand } from '../../commands/commands/delete-sheet-table.command';
import { SetSheetTableCommand } from '../../commands/commands/set-sheet-table.command';
import { SheetTableInsertColumnAtCommand, SheetTableRemoveColumnAtCommand } from '../../commands/commands/sheet-table-row-col.command';
import { AddSheetTableMutation } from '../../commands/mutations/add-sheet-table.mutation';
import { DeleteSheetTableMutation } from '../../commands/mutations/delete-sheet-table.mutation';
import { SetSheetTableMutation } from '../../commands/mutations/set-sheet-table.mutation';
import { TableManager } from '../../models/table-manager';
import { SheetTableService } from '../../services/table.service';
import { SheetTableFormulaController } from '../sheet-table-formula.controller';
import { SheetTableRefRangeController } from '../sheet-table-ref-range.controller';
interface ITestBed {
univer: Univer;
sheet: UnitModel<IWorkbookData>;
injector: Injector;
}
class RenderManagerServiceTestBed extends RenderManagerService {
override createRender(unitId: string): IRender {
const renderer = this._createRender(unitId, new Engine('', { elementHeight: 100, elementWidth: 100 }));
return renderer;
}
}
function createFacadeTestBed(workbookData: IWorkbookData, dependencies: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([SheetsSelectionsService]);
injector.add([SheetInterceptorService]);
injector.add([IFunctionService, { useClass: FunctionService }]);
injector.add([IRenderManagerService, { useClass: RenderManagerServiceTestBed }]);
injector.add([SheetSkeletonService]);
injector.add([FormulaDataModel]);
injector.add([LexerTreeBuilder]);
injector.add([RefRangeService]);
injector.add([WorksheetPermissionService]);
injector.add([WorkbookPermissionService]);
injector.add([WorksheetProtectionPointModel]);
injector.add([RangeProtectionRuleModel]);
injector.add([WorksheetProtectionRuleModel]);
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
dependencies.forEach((d) => injector.add(d));
this._injector.get(SheetInterceptorService);
this._injector.get(WorkbookPermissionService);
this._injector.get(WorksheetPermissionService);
}
}
injector.get(LocaleService).load({ zhCN, enUS });
const themeService = injector.get(ThemeService);
const theme = themeService.getCurrentTheme();
const newTheme = set(theme, 'gray.1000', '#35322b');
themeService.setTheme(newTheme);
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData);
injector.get(IUniverInstanceService).focusUnit('test');
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
return {
univer,
sheet,
injector,
};
}
function createWorkbookData(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: 'test',
sheetOrder: ['sheet1', 'sheet2'],
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
rowCount: 20,
columnCount: 20,
cellData: {
0: {
0: { v: 'product', t: CellValueType.STRING },
1: { v: 'amount', t: CellValueType.STRING },
2: { v: 'tax', t: CellValueType.STRING },
},
1: {
0: { v: 'A', t: CellValueType.STRING },
1: { v: 10, t: CellValueType.NUMBER },
2: { v: 1, t: CellValueType.NUMBER },
},
2: {
0: { v: 'B', t: CellValueType.STRING },
1: { v: 20, t: CellValueType.NUMBER },
2: { v: 2, t: CellValueType.NUMBER },
},
3: {
0: { v: 'C', t: CellValueType.STRING },
1: { v: 30, t: CellValueType.NUMBER },
2: { v: 3, t: CellValueType.NUMBER },
},
5: {
4: { f: '=SUM(Orders[amount])' },
5: { f: '="Orders[amount]"' },
6: { f: '=Table[1]' },
7: { f: '=Table[2]' },
8: { f: '="Table[1]"' },
},
6: {
4: { f: '=SUM(Orders[amount],Orders[amount])' },
},
7: {
4: { f: '=SUM(Orders[amount])' },
},
},
},
sheet2: {
id: 'sheet2',
name: 'Sheet2',
rowCount: 20,
columnCount: 20,
cellData: {
2: {
0: { f: '=SUM(Orders)' },
},
3: {
0: { f: '=SUM(Orders[amount])' },
},
4: {
0: { f: '=SUM(Orders[amount],Orders[tax])' },
},
},
},
},
styles: {},
};
}
function createControllerTestBed() {
const dependencies: Dependency[] = [
[ICalculateFormulaService, { useClass: CalculateFormulaService }],
[Lexer],
[IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }],
[IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }],
[IFormulaExternalReferenceDataLoader, { useClass: NoopFormulaExternalReferenceDataLoader }],
[IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }],
[IFormulaRuntimeService, { useClass: FormulaRuntimeService }],
[IOtherFormulaManagerService, { useClass: OtherFormulaManagerService }],
[ISheetRowFilteredService, { useClass: SheetRowFilteredService }],
[IFormulaDependencyGenerator, { useClass: FormulaDependencyGenerator }],
[Interpreter],
[AstTreeBuilder],
[AstRootNodeFactory],
[FunctionNodeFactory],
[LambdaNodeFactory],
[LambdaParameterNodeFactory],
[OperatorNodeFactory],
[PrefixNodeFactory],
[ReferenceNodeFactory],
[SuffixNodeFactory],
[UnionNodeFactory],
[ValueNodeFactory],
[ISuperTableService, { useClass: SuperTableService }],
[TableManager],
[SheetTableService],
[SheetTableFormulaController],
[SheetTableRefRangeController],
[UpdateFormulaController],
];
return createFacadeTestBed(createWorkbookData(), dependencies);
}
describe('Sheet table formula integration', () => {
let testBed: ReturnType<typeof createControllerTestBed>;
let commandService: ICommandService;
let formulaDataModel: FormulaDataModel;
let calculate: (formula: string, row?: number, column?: number, sheetId?: string) => string | number | boolean | null | (string | number | boolean | null)[][];
function getFormula(unitId: string, sheetId: string, row: number, column: number) {
return formulaDataModel.getFormulaData()[unitId]?.[sheetId]?.[row]?.[column]?.f;
}
function getCellFormula(sheetId: string, row: number, column: number) {
return testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId(sheetId)
?.getRange(row, column, row, column)
.getValue()
?.f;
}
function getAllUnitData() {
const workbook = testBed.injector.get(IUniverInstanceService).getUnit<Workbook>('test')!;
const sheetData: IUnitData[string] = {};
workbook.getSheets().forEach((sheet) => {
const sheetConfig = sheet.getConfig();
sheetData[sheet.getSheetId()] = {
cellData: new ObjectMatrix(sheetConfig.cellData),
rowCount: sheetConfig.rowCount,
columnCount: sheetConfig.columnCount,
rowData: sheetConfig.rowData ?? {},
columnData: sheetConfig.columnData ?? {},
};
});
return {
[workbook.getUnitId()]: sheetData,
};
}
function calculateCellFormula(unitId: string, sheetId: string, row: number, column: number) {
const formula = getFormula(unitId, sheetId, row, column);
expect(formula).toBeDefined();
return calculate(formula!, row, column, sheetId);
}
beforeEach(() => {
testBed = createControllerTestBed();
commandService = testBed.injector.get(ICommandService);
formulaDataModel = testBed.injector.get(FormulaDataModel);
commandService.registerCommand(RemoveRowCommand);
commandService.registerCommand(RemoveColCommand);
commandService.registerCommand(MoveRangeCommand);
commandService.registerCommand(MoveRangeMutation);
commandService.registerCommand(RemoveRowByRangeCommand);
commandService.registerCommand(RemoveColByRangeCommand);
commandService.registerCommand(RemoveRowMutation);
commandService.registerCommand(RemoveColMutation);
commandService.registerCommand(InsertRowMutation);
commandService.registerCommand(InsertColMutation);
commandService.registerCommand(AddSheetTableMutation);
commandService.registerCommand(DeleteSheetTableCommand);
commandService.registerCommand(DeleteSheetTableMutation);
commandService.registerCommand(SetSheetTableCommand);
commandService.registerCommand(SetSheetTableMutation);
commandService.registerCommand(SheetTableInsertColumnAtCommand);
commandService.registerCommand(SheetTableRemoveColumnAtCommand);
commandService.registerCommand(SetSuperTableMutation);
commandService.registerCommand(RemoveSuperTableMutation);
commandService.registerCommand(SetRangeValuesCommand);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(SetFormulaDataMutation);
commandService.registerCommand(SetArrayFormulaDataMutation);
testBed.injector.get(SheetTableFormulaController);
testBed.injector.get(SheetTableRefRangeController);
testBed.injector.get(UpdateFormulaController);
commandService.syncExecuteCommand(AddSheetTableMutation.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
name: 'Orders',
range: {
startRow: 0,
endRow: 3,
startColumn: 0,
endColumn: 2,
},
header: ['product', 'amount', 'tax'],
options: {
columns: [
{ id: 'product-column', displayName: 'product' },
{ id: 'amount-column', displayName: 'amount' },
{ id: 'tax-column', displayName: 'tax' },
],
},
});
const functionService = testBed.injector.get(IFunctionService);
const mathFunctions = functionMath as Array<[new (name: string) => BaseFunction, string]>;
functionService.registerExecutors(...mathFunctions.map(([FunctionCtor, name]) => new FunctionCtor(name)));
calculate = (formula: string, row = 0, column = 0, sheetId = 'sheet1') => {
const workbook = testBed.injector.get(IUniverInstanceService).getUnit<Workbook>('test')!;
const worksheet = workbook.getSheetBySheetId(sheetId)!;
const formulaCurrentConfigService = testBed.injector.get(IFormulaCurrentConfigService);
const formulaRuntimeService = testBed.injector.get(IFormulaRuntimeService);
formulaCurrentConfigService.load({
formulaData: formulaDataModel.getFormulaData(),
arrayFormulaCellData: {},
arrayFormulaRange: {},
forceCalculate: false,
dirtyRanges: [],
dirtyNameMap: {},
dirtyDefinedNameMap: {},
dirtySuperTableMap: {},
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
excludedCell: {},
allUnitData: getAllUnitData(),
});
formulaRuntimeService.setCurrent(row, column, worksheet.getRowCount(), worksheet.getColumnCount(), sheetId, workbook.getUnitId());
const lexerNode = testBed.injector.get(Lexer).treeBuilder(formula);
if (!(lexerNode instanceof LexerNode)) {
throw new TypeError(`Failed to parse formula: ${formula}`);
}
const astNode = testBed.injector.get(AstTreeBuilder).parse(lexerNode);
const result = testBed.injector.get(Interpreter).execute(generateExecuteAstNodeData(astNode as BaseAstNode));
return getObjectValue(result);
};
});
afterEach(() => {
vi.restoreAllMocks();
testBed?.univer.dispose();
});
it('should sync table formulas when a table is renamed, undone, and redone', async () => {
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
unitId: 'test',
tableId: 'orders-table',
name: 'Sales',
})).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
});
it('should sync wrapped repeated table references when a table is renamed, removed, and has a referenced column removed', async () => {
expect(calculateCellFormula('test', 'sheet1', 6, 4)).toBe(120);
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
unitId: 'test',
tableId: 'orders-table',
name: 'Sales',
})).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
expect(calculateCellFormula('test', 'sheet1', 6, 4)).toBe(120);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
index: 1,
count: 1,
})).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
})).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
});
it('should sync multiple table formulas across sheets when a table changes, then undo and redo', async () => {
expect(calculateCellFormula('test', 'sheet1', 7, 4)).toBe(60);
expect(calculateCellFormula('test', 'sheet2', 3, 0)).toBe(60);
expect(calculateCellFormula('test', 'sheet2', 4, 0)).toBe(66);
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
unitId: 'test',
tableId: 'orders-table',
name: 'Sales',
})).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Sales[amount])');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Sales[amount])');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Sales[amount],Sales[tax])');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Sales[amount])');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Sales[amount])');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Sales[amount],Sales[tax])');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
index: 1,
count: 1,
})).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,Orders[tax])');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,Orders[tax])');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
})).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,#REF!)');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,#REF!)');
});
it('should recalculate table formulas when table columns shrink, then undo and redo the column insertion', async () => {
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
index: 2,
count: 1,
})).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(60);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(60);
});
it('should recalculate table formulas when table columns are inserted, undone, and redone', async () => {
const tableManager = testBed.injector.get(TableManager);
const getTableRange = () => tableManager.getTableById('test', 'orders-table')?.getRange();
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 2 });
expect(await commandService.executeCommand(SheetTableInsertColumnAtCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
index: 2,
count: 1,
})).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 3 });
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 2 });
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 3 });
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
});
it('should recalculate table column formulas when worksheet rows are removed, undone, and redone', async () => {
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RemoveRowCommand.id, {
range: {
startRow: 2,
endRow: 2,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
},
})).toBe(true);
expect(getFormula('test', 'sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
expect(getCellFormula('sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
expect(calculateCellFormula('test', 'sheet1', 4, 4)).toBe(40);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
expect(getCellFormula('sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
expect(calculateCellFormula('test', 'sheet1', 4, 4)).toBe(40);
});
it('should sync table column formulas when worksheet columns are removed, undone, and redone', async () => {
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RemoveColCommand.id, {
range: {
startRow: 0,
endRow: 19,
startColumn: 1,
endColumn: 1,
rangeType: RANGE_TYPE.COLUMN,
},
})).toBe(true);
expect(getFormula('test', 'sheet1', 5, 3)).toBe('=SUM(#REF!)');
expect(getCellFormula('sheet1', 5, 3)).toBe('=SUM(#REF!)');
expect(calculate('=SUM(#REF!)', 5, 3)).toBe('#REF!');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 3)).toBe('=SUM(#REF!)');
expect(getCellFormula('sheet1', 5, 3)).toBe('=SUM(#REF!)');
expect(calculate('=SUM(#REF!)', 5, 3)).toBe('#REF!');
});
it('should sync table formulas when a table is deleted, undone, and redone', async () => {
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'orders-table',
})).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(#REF!)');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(#REF!)');
expect(calculate('=SUM(#REF!)', 5, 4)).toBe('#REF!');
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(#REF!)');
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(#REF!)');
expect(calculate('=SUM(#REF!)', 5, 4)).toBe('#REF!');
});
it('should rewrite formulas when a referenced table is renamed', async () => {
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
const renameTableResult = sheetInterceptorService.onCommandExecute({
id: 'sheet.command.set-table-config',
params: {
unitId: 'test',
tableId: 'table-1',
name: 'Sales',
oldTableName: 'Orders',
},
});
for (const mutation of renameTableResult.redos) {
await commandService.executeCommand(mutation.id, mutation.params);
}
const values = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 4, 5, 4)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(values).toEqual([[{ f: '=SUM(Sales[amount])' }]]);
const stringLiteralValues = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 5, 5, 5)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(stringLiteralValues).toEqual([[{ f: '="Orders[amount]"' }]]);
});
it('should rewrite formulas when a referenced table is removed', async () => {
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
const removeTableResult = sheetInterceptorService.onCommandExecute({
id: 'sheet.command.delete-table',
params: {
unitId: 'test',
tableId: 'table-1',
tableName: 'Orders',
},
});
for (const mutation of removeTableResult.redos) {
await commandService.executeCommand(mutation.id, mutation.params);
}
const values = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 4, 5, 4)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(values).toEqual([[{ f: '=SUM(#REF!)' }]]);
const stringLiteralValues = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 5, 5, 5)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(stringLiteralValues).toEqual([[{ f: '="Orders[amount]"' }]]);
});
it('should rewrite numeric table column references when the referenced table column is removed', async () => {
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
const removeTableColumnResult = sheetInterceptorService.onCommandExecute({
id: 'sheet.command.table-remove-column-at',
params: {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'table-1',
tableName: 'Table',
removedColumnNames: ['1'],
},
});
for (const mutation of removeTableColumnResult.redos) {
await commandService.executeCommand(mutation.id, mutation.params);
}
const values = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 6, 5, 8)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(values).toEqual([[
{ f: '=#REF!' },
{ f: '=Table[2]' },
{ f: '="Table[1]"' },
]]);
});
it('should move formula cells when worksheet column removal deletes a referenced table column', async () => {
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
const removeTableColumnResult = sheetInterceptorService.onCommandExecute({
id: 'sheet.command.table-remove-col',
params: {
unitId: 'test',
subUnitId: 'sheet1',
tableId: 'table-1',
tableName: 'Table',
range: { startRow: 0, endRow: 19, startColumn: 0, endColumn: 0 },
removedColumnNames: ['1'],
},
});
for (const mutation of removeTableColumnResult.redos) {
await commandService.executeCommand(mutation.id, mutation.params);
}
const values = testBed.injector
.get(IUniverInstanceService)
.getUnit<Workbook>('test')
?.getSheetBySheetId('sheet1')
?.getRange(5, 5, 5, 8)
.getValues() as Array<Array<Nullable<ICellData>>>;
expect(values).toEqual([[{ f: '=#REF!' }, { f: '=Table[2]' }, { f: '="Table[1]"' }, null]]);
});
});

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { FEATURE_TABLE_ID } from '../../const';
import { SheetTableRangeController } from '../sheet-table-range.controller';
describe('SheetTableRangeController', () => {
it('should sync exclusive ranges on table add/range change/delete', () => {
const tableRangeChanged$ = new Subject<any>();
const tableAdd$ = new Subject<any>();
const tableDelete$ = new Subject<any>();
const tableManager = {
tableRangeChanged$,
tableAdd$,
tableDelete$,
};
const exclusiveRangeService = {
clearExclusiveRangesByGroupId: vi.fn(),
addExclusiveRange: vi.fn(),
};
const controller = new SheetTableRangeController(tableManager as any, exclusiveRangeService as any);
tableAdd$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 },
});
expect(exclusiveRangeService.addExclusiveRange).toHaveBeenCalledWith('u1', 's1', FEATURE_TABLE_ID, [{
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 },
groupId: 't1',
}]);
tableRangeChanged$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
range: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 },
});
expect(exclusiveRangeService.clearExclusiveRangesByGroupId).toHaveBeenCalledWith('u1', 's1', FEATURE_TABLE_ID, 't1');
expect(exclusiveRangeService.addExclusiveRange).toHaveBeenLastCalledWith('u1', 's1', FEATURE_TABLE_ID, [{
range: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 },
groupId: 't1',
}]);
tableDelete$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
});
expect(exclusiveRangeService.clearExclusiveRangesByGroupId).toHaveBeenLastCalledWith('u1', 's1', FEATURE_TABLE_ID, 't1');
controller.dispose();
});
});

View File

@@ -0,0 +1,162 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InsertRowMutation, RemoveColCommand } from '@univerjs/sheets';
import { describe, expect, it, vi } from 'vitest';
import { SheetTableRefRangeController } from '../sheet-table-ref-range.controller';
describe('SheetTableRefRangeController', () => {
it('should return empty mutations for unsupported commands and update table range after row insertion', () => {
let interceptCommandConfig: any;
let commandExecutedListener: any;
const table = {
getId: () => 't1',
getRange: () => ({ startRow: 5, endRow: 10, startColumn: 1, endColumn: 3 }),
};
const tableManager = {
getTablesBySubunitId: vi.fn(() => [table]),
updateTableRange: vi.fn(),
};
const controller = new SheetTableRefRangeController(
{
onCommandExecuted: vi.fn((listener: any) => {
commandExecutedListener = listener;
return { dispose: vi.fn() };
}),
} as any,
{} as any,
{} as any,
{} as any,
{
interceptCommand: vi.fn((config: any) => {
interceptCommandConfig = config;
return { dispose: vi.fn() };
}),
} as any,
tableManager as any,
{ t: () => 'Column' } as any
);
expect(interceptCommandConfig.getMutations({ id: 'unknown.command', params: {} })).toEqual({ redos: [], undos: [] });
commandExecutedListener({
id: InsertRowMutation.id,
params: {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 2, startColumn: 0, endColumn: 10 },
},
});
expect(tableManager.updateTableRange).toHaveBeenCalledWith('u1', 't1', {
newRange: {
startRow: 7,
endRow: 12,
startColumn: 1,
endColumn: 3,
},
});
controller.dispose();
});
it('should include table formula reference mutations when worksheet column removal deletes table columns', () => {
let interceptCommandConfig: any;
const tableColumn = {
toJSON: () => ({ id: 'c1', displayName: '1' }),
};
const table = {
getId: () => 't1',
getRange: () => ({ startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 }),
getTableInfo: () => ({ name: 'Table' }),
getTableColumnByIndex: vi.fn(() => tableColumn),
toJSON: () => ({
id: 't1',
name: 'Table',
range: { startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 },
options: { showHeader: true },
filters: {},
columns: [{ id: 'c1', displayName: '1' }, { id: 'c2', displayName: '2' }, { id: 'c3', displayName: '3' }],
}),
};
const workbook = {
getUnitId: () => 'u1',
getActiveSheet: () => ({ getSheetId: () => 's1' }),
};
const onCommandExecute = vi.fn(() => ({
preRedos: [{ id: 'formula.pre-redo', params: {} }],
redos: [{ id: 'formula.redo', params: {} }],
preUndos: [{ id: 'formula.pre-undo', params: {} }],
undos: [{ id: 'formula.undo', params: {} }],
}));
const controller = new SheetTableRefRangeController(
{ onCommandExecuted: vi.fn(() => ({ dispose: vi.fn() })) } as any,
{} as any,
{ getCurrentUnitOfType: () => workbook } as any,
{} as any,
{
interceptCommand: vi.fn((config: any) => {
interceptCommandConfig = config;
return { dispose: vi.fn() };
}),
onCommandExecute,
} as any,
{
getTablesBySubunitId: vi.fn(() => [table]),
} as any,
{ t: () => 'Column' } as any
);
const result = interceptCommandConfig.getMutations({
id: RemoveColCommand.id,
params: {
range: { startRow: 0, endRow: 99, startColumn: 1, endColumn: 1 },
},
});
expect(onCommandExecute).toHaveBeenCalledWith({
id: 'sheet.command.table-remove-col',
params: {
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
tableName: 'Table',
range: { startRow: 0, endRow: 99, startColumn: 1, endColumn: 1 },
removedColumnNames: ['1'],
},
});
expect(result.preRedos.map((mutation: any) => mutation.id)).toEqual([]);
expect(result.redos.map((mutation: any) => mutation.id)).toEqual([
'formula.pre-redo',
'formula.redo',
'sheet.mutation.set-sheet-table',
]);
expect(result.preUndos.map((mutation: any) => mutation.id)).toEqual([
'sheet.mutation.delete-table',
]);
expect(result.undos.map((mutation: any) => mutation.id)).toEqual([
'sheet.mutation.add-table',
'formula.pre-undo',
'formula.undo',
]);
controller.dispose();
});
});

View File

@@ -0,0 +1,132 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { SheetsTableThemeController } from '../sheet-table-theme.controller';
describe('SheetsTableThemeController', () => {
it('should initialize themes and react to table theme lifecycle events', () => {
const tableAdd$ = new Subject<any>();
const tableRangeChanged$ = new Subject<any>();
const tableThemeChanged$ = new Subject<any>();
const tableDelete$ = new Subject<any>();
const table = {
getTableStyleId: vi.fn(() => undefined),
setTableStyleId: vi.fn(),
getRange: vi.fn(() => ({ startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 })),
};
const tableManager = {
tableAdd$,
tableRangeChanged$,
tableThemeChanged$,
tableDelete$,
getTable: vi.fn(() => table),
};
const sheetRangeThemeService = {
registerRangeThemeStyle: vi.fn(),
removeRangeThemeRule: vi.fn(),
};
const sheetRangeThemeModel = {
registerDefaultRangeTheme: vi.fn(),
};
const configService = {
getConfig: vi.fn(() => ({
defaultThemeIndex: 1,
userThemes: [{ name: 'user-theme', style: {} }],
})),
};
const controller = new SheetsTableThemeController(
tableManager as any,
sheetRangeThemeService as any,
sheetRangeThemeModel as any,
configService as any
);
expect(sheetRangeThemeModel.registerDefaultRangeTheme).toHaveBeenCalledTimes(7);
tableAdd$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
tableStyleId: '',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
expect(table.setTableStyleId).toHaveBeenCalledWith('table-default-0');
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-0', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
tableRangeChanged$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
oldRange: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
range: { startRow: 1, endRow: 5, startColumn: 1, endColumn: 2 },
});
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-0', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 5, startColumn: 1, endColumn: 2 },
});
tableThemeChanged$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
oldTheme: 'table-default-0',
theme: 'table-default-1',
});
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-1', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
tableDelete$.next({
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
unitId: 'u1',
subUnitId: 's1',
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
});
controller.dispose();
});
});

View File

@@ -0,0 +1,106 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { SheetsTableController } from '../sheets-table.controller';
describe('SheetsTableController', () => {
it('should resolve containing table by range and inject header cell content', () => {
const tableAdd$ = new Subject<any>();
const tableRangeChanged$ = new Subject<any>();
const tableDelete$ = new Subject<any>();
const tableRange = { startRow: 2, endRow: 6, startColumn: 1, endColumn: 4 };
const table = {
getRange: () => tableRange,
getColumnNameByIndex: (index: number) => ['Name', 'Qty', 'Price', 'Note'][index],
};
const tableManager = {
tableAdd$,
tableRangeChanged$,
tableDelete$,
getTable: vi.fn((_unitId: string, tableId: string) => (tableId === 't1' ? table : undefined)),
getTablesBySubunitId: vi.fn(() => []),
toJSON: vi.fn(() => ({})),
fromJSON: vi.fn(),
deleteUnitId: vi.fn(),
};
let cellContentHandler: any;
const sheetInterceptorService = {
intercept: vi.fn((_point: any, config: any) => {
cellContentHandler = config.handler;
return { dispose: vi.fn() };
}),
interceptCommand: vi.fn(() => ({ dispose: vi.fn() })),
};
const resourceManagerService = {
registerPluginResource: vi.fn(() => ({ dispose: vi.fn() })),
};
const controller = new SheetsTableController(
{
getCurrentUnitOfType: vi.fn(() => null),
} as any,
sheetInterceptorService as any,
tableManager as any,
resourceManagerService as any
);
tableAdd$.next({
unitId: 'u1',
subUnitId: 's1',
tableId: 't1',
range: tableRange,
});
expect(
controller.getContainerTableWithRange('u1', 's1', { startRow: 3, endRow: 4, startColumn: 2, endColumn: 3 })
).toBe(table);
expect(
controller.getContainerTableWithRange('u1', 's1', { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })
).toBeUndefined();
const next = vi.fn((cell) => cell);
const result = cellContentHandler(undefined, {
row: 2,
col: 2,
unitId: 'u1',
subUnitId: 's1',
rawData: { s: 1 },
}, next);
expect(next).toHaveBeenCalledWith({ s: 1, v: 'Qty' });
expect(result).toEqual({ s: 1, v: 'Qty' });
const nextNonHeader = vi.fn((cell) => cell);
cellContentHandler(undefined, {
row: 3,
col: 2,
unitId: 'u1',
subUnitId: 's1',
rawData: { s: 1 },
}, nextNonHeader);
expect(nextNonHeader).toHaveBeenCalledWith(undefined);
controller.dispose();
});
});

View File

@@ -0,0 +1,104 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { TableFilterController } from '../table-filter.controller';
describe('TableFilterController', () => {
it('should merge filtered rows from table filter events and initialize intercept behavior', () => {
const tableFilterChanged$ = new Subject<any>();
const tableInitStatus$ = new BehaviorSubject(false);
const mainFilter = {
doFilter: vi.fn(),
getFilterOutRows: () => new Set([4]),
};
const mainTable = {
getTableFilters: () => mainFilter,
getTableFilterRange: () => ({ startRow: 1, endRow: 10, startColumn: 1, endColumn: 2 }),
};
const extraTable = {
getTableFilters: () => ({
getFilterOutRows: () => new Set([8]),
}),
};
const tableManager = {
tableFilterChanged$,
tableInitStatus$,
getTable: vi.fn(() => mainTable),
getTablesBySubunitId: vi.fn(() => [mainTable, extraTable]),
};
let rowFilteredHandler: any;
const sheetInterceptorService = {
intercept: vi.fn((_point: any, config: any) => {
rowFilteredHandler = config.handler;
return { dispose: vi.fn() };
}),
};
const activeSheet$ = new Subject<any>();
const workbook = {
getUnitId: () => 'u1',
getActiveSheet: () => ({ getSheetId: () => 's1' }),
activeSheet$,
getSheetBySheetId: vi.fn(() => ({ id: 'sheet1' })),
getDateSystem: () => DateSystem.Date1904,
};
const workbookType$ = new Subject<any>();
const univerInstanceService = {
getCurrentTypeOfUnit$: vi.fn(() => workbookType$),
getCurrentUnitOfType: vi.fn(() => workbook),
getUnit: vi.fn(() => workbook),
};
const zebraCrossingCacheController = {
updateZebraCrossingCache: vi.fn(),
};
const controller = new TableFilterController(
tableManager as any,
sheetInterceptorService as any,
univerInstanceService as any,
zebraCrossingCacheController as any
);
tableFilterChanged$.next({ unitId: 'u1', subUnitId: 's1', tableId: 't1' });
expect(mainFilter.doFilter).toHaveBeenCalledWith(
{ id: 'sheet1' },
{ startRow: 1, endRow: 10, startColumn: 1, endColumn: 2 },
DateSystem.Date1904
);
expect(zebraCrossingCacheController.updateZebraCrossingCache).toHaveBeenCalledWith('u1', 's1');
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 4 }, vi.fn(() => false))).toBe(true);
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's2', row: 4 }, vi.fn(() => false))).toBe(false);
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 9 }, vi.fn(() => false))).toBe(false);
tableInitStatus$.next(true);
workbookType$.next(workbook);
activeSheet$.next({ getSheetId: () => 's1' });
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 8 }, vi.fn(() => false))).toBe(true);
controller.dispose();
});
});

View File

@@ -0,0 +1,60 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, expect, it } from 'vitest';
import { tableDefaultBorderStyle } from '../../config/config';
import { customEmptyThemeWithBorderStyle, processStyleWithBorderStyle, tableThemeConfig } from '../table-theme.factory';
describe('table-theme.factory', () => {
it('should fill border style for specific theme keys when border is missing', () => {
expect(processStyleWithBorderStyle('headerRowStyle', {} as any)).toEqual({
bd: { t: tableDefaultBorderStyle },
});
expect(processStyleWithBorderStyle('lastRowStyle', {} as any)).toEqual({
bd: { b: tableDefaultBorderStyle },
});
expect(processStyleWithBorderStyle('lastColumnStyle', {} as any)).toEqual({
bd: { r: tableDefaultBorderStyle },
});
expect(processStyleWithBorderStyle('headerColumnStyle', {} as any)).toEqual({
bd: { l: tableDefaultBorderStyle },
});
});
it('should not override an existing border definition', () => {
const style = { bd: { t: { s: 0 } }, bg: { rgb: '#fff' } } as any;
expect(processStyleWithBorderStyle('headerRowStyle', style)).toBe(style);
});
it('should expose the default empty theme with four-side border', () => {
expect(customEmptyThemeWithBorderStyle.headerRowStyle?.bd?.t).toEqual(tableDefaultBorderStyle);
expect(customEmptyThemeWithBorderStyle.headerColumnStyle?.bd?.l).toEqual(tableDefaultBorderStyle);
expect(customEmptyThemeWithBorderStyle.lastColumnStyle?.bd?.r).toEqual(tableDefaultBorderStyle);
expect(customEmptyThemeWithBorderStyle.lastRowStyle?.bd?.b).toEqual(tableDefaultBorderStyle);
});
it('should generate built-in table themes with stable naming and border defaults', () => {
expect(tableThemeConfig).toHaveLength(6);
expect(tableThemeConfig[0].name).toBe('table-default-0');
expect(tableThemeConfig[5].name).toBe('table-default-5');
expect(tableThemeConfig[0].style.headerRowStyle?.bd?.t).toEqual(tableDefaultBorderStyle);
expect(tableThemeConfig[0].style.lastColumnStyle?.bd?.r).toEqual(tableDefaultBorderStyle);
expect(tableThemeConfig[0].style.secondRowStyle?.bg?.rgb).toBe('#EEF2FF');
});
});

View File

@@ -0,0 +1,103 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRemoveSuperTableMutationParam, ISetSuperTableMutationParam, ISetSuperTableMutationSearchParam } from '@univerjs/engine-formula';
import type { Table } from '../models/table';
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { RemoveSuperTableMutation, SetSuperTableMutation } from '@univerjs/engine-formula';
import { TableManager } from '../models/table-manager';
export class SheetTableFormulaController extends Disposable {
constructor(
@Inject(TableManager) private _tableManager: TableManager,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initRangeListener();
}
private _initRangeListener() {
this.disposeWithMe(
this._tableManager.tableRangeChanged$.subscribe((event) => {
const { tableId, unitId } = event;
const table = this._tableManager.getTableById(unitId, tableId);
if (!table) {
return;
}
this._updateSuperTable(unitId, table);
})
);
this.disposeWithMe(
this._tableManager.tableAdd$.subscribe((event) => {
const { tableId, unitId } = event;
const table = this._tableManager.getTableById(unitId, tableId);
if (!table) {
return;
}
this._updateSuperTable(unitId, table);
})
);
this.disposeWithMe(
this._tableManager.tableDelete$.subscribe((event) => {
const { unitId, subUnitId, tableName, range } = event;
this._commandService.executeCommand<IRemoveSuperTableMutationParam>(RemoveSuperTableMutation.id, {
unitId,
tableName,
reference: {
sheetId: subUnitId,
range,
},
}, { onlyLocal: true });
})
);
this.disposeWithMe(
this._tableManager.tableNameChanged$.subscribe((event) => {
const { tableId, unitId, oldTableName } = event;
this._commandService.executeCommand<ISetSuperTableMutationSearchParam>(RemoveSuperTableMutation.id, {
unitId,
tableName: oldTableName,
}, { onlyLocal: true });
const table = this._tableManager.getTableById(unitId, tableId);
if (!table) {
return;
}
this._updateSuperTable(unitId, table, oldTableName);
})
);
}
private _updateSuperTable(unitId: string, table: Table, oldTableName?: string) {
const tableInfo = table.getTableInfo();
const name = tableInfo.name;
const columns = tableInfo.columns;
const titleMap = new Map<string, number>();
columns.forEach((column, index) => {
titleMap.set(column.displayName, index);
});
this._commandService.executeCommand<ISetSuperTableMutationParam>(SetSuperTableMutation.id, {
unitId,
tableName: name,
oldTableName,
reference: {
range: tableInfo.range,
sheetId: tableInfo.subUnitId,
titleMap,
},
}, { onlyLocal: true });
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { IExclusiveRangeService } from '@univerjs/sheets';
import { FEATURE_TABLE_ID } from '../const';
import { TableManager } from '../models/table-manager';
export class SheetTableRangeController extends Disposable {
constructor(
@Inject(TableManager) private _tableManager: TableManager,
@Inject(IExclusiveRangeService) private _exclusiveRangeService: IExclusiveRangeService
) {
super();
this._initRangeListener();
}
private _initRangeListener() {
this.disposeWithMe(
this._tableManager.tableRangeChanged$.subscribe((event) => {
const { range, tableId, unitId, subUnitId } = event;
this._exclusiveRangeService.clearExclusiveRangesByGroupId(unitId, subUnitId, FEATURE_TABLE_ID, tableId);
this._exclusiveRangeService.addExclusiveRange(unitId, subUnitId, FEATURE_TABLE_ID, [{
range: { ...range },
groupId: tableId,
}]);
})
);
this.disposeWithMe(
this._tableManager.tableAdd$.subscribe((event) => {
const { tableId, unitId, subUnitId, range } = event;
this._exclusiveRangeService.addExclusiveRange(unitId, subUnitId, FEATURE_TABLE_ID, [{
range: { ...range },
groupId: tableId,
}]);
})
);
this.disposeWithMe(
this._tableManager.tableDelete$.subscribe((event) => {
const { tableId, unitId, subUnitId } = event;
this._exclusiveRangeService.clearExclusiveRangesByGroupId(unitId, subUnitId, FEATURE_TABLE_ID, tableId);
})
);
}
}

View File

@@ -0,0 +1,647 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IMutationInfo, Workbook } from '@univerjs/core';
import type {
IInsertColCommandParams,
IInsertRowCommandParams,
IInsertRowMutationParams,
IRemoveRowColCommandParams,
} from '@univerjs/sheets';
import type { LocaleKey } from '../locale/types';
import type { ITableColumnJson, ITableJson } from '../types/type';
import {
Disposable,
ICommandService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
Rectangle,
} from '@univerjs/core';
import {
getSheetCommandTarget,
InsertColCommand,
InsertColMutation,
InsertRowCommand,
InsertRowMutation,
RefRangeService,
RemoveColCommand,
RemoveColMutation,
RemoveRowCommand,
RemoveRowMutation,
SheetInterceptorService,
} from '@univerjs/sheets';
import { AddSheetTableMutation } from '../commands/mutations/add-sheet-table.mutation';
import { DeleteSheetTableMutation } from '../commands/mutations/delete-sheet-table.mutation';
import { SetSheetTableMutation } from '../commands/mutations/set-sheet-table.mutation';
import { TableManager } from '../models/table-manager';
import { IRangeOperationTypeEnum, IRowColTypeEnum } from '../types/type';
import { convertCellDataToString, getColumnName } from '../util';
const SHEET_TABLE_REMOVE_COL_COMMAND_ID = 'sheet.command.table-remove-col';
const DELETE_SHEET_TABLE_COMMAND_ID = 'sheet.command.delete-table';
export class SheetTableRefRangeController extends Disposable {
constructor(
@Inject(ICommandService) private readonly _commandService: ICommandService,
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
@Inject(Injector) private _injector: Injector,
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(TableManager) private _tableManager: TableManager,
@Inject(LocaleService) private _localeService: LocaleService
) {
super();
this._initCommandInterceptor();
this._initCommandListener();
}
private _initCommandInterceptor() {
const self = this;
this._sheetInterceptorService.interceptCommand({
priority: -1,
getMutations(commandInfo) {
const defaultReturn = { redos: [], undos: [] };
const { id, params } = commandInfo;
switch (id) {
case InsertRowCommand.id:
return self._generateTableMutationWithInsertRow(params as IInsertRowCommandParams);
case InsertColCommand.id:
return self._generateTableMutationWithInsertCol(params as IInsertColCommandParams);
case RemoveRowCommand.id:
return self._generateTableMutationWithRemoveRow(params as IRemoveRowColCommandParams);
case RemoveColCommand.id:
return self._generateTableMutationWithRemoveCol(params as IRemoveRowColCommandParams);
}
return defaultReturn;
},
});
}
private _generateTableMutationWithInsertRow(insertParams: IInsertRowCommandParams) {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const target = getSheetCommandTarget(this._univerInstanceService, insertParams);
if (!target) {
return { undos, redos };
}
const { unitId, subUnitId } = target;
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (!allSubUnitTables.length) {
return { undos, redos };
}
const { range } = insertParams;
allSubUnitTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startRow > tableRange.startRow && range.startRow <= tableRange.endRow) {
const insertRowCount = range.endRow - range.startRow + 1;
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
endRow: tableRange.endRow + insertRowCount,
},
},
},
},
});
undos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
},
},
},
},
});
}
});
return { undos, redos };
}
private _generateTableMutationWithInsertCol(insertParams: IInsertColCommandParams) {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const target = getSheetCommandTarget(this._univerInstanceService, insertParams);
if (!target) {
return { undos, redos };
}
const { unitId, subUnitId } = target;
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (!allSubUnitTables.length) {
return { undos, redos };
}
const { range } = insertParams;
allSubUnitTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startColumn > tableRange.startColumn && range.startColumn <= tableRange.endColumn) {
const insertColCount = range.endColumn - range.startColumn + 1;
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
rowColOperation: {
operationType: IRangeOperationTypeEnum.Insert,
rowColType: IRowColTypeEnum.Col,
index: range.startColumn,
count: insertColCount,
},
},
},
});
undos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
rowColOperation: {
operationType: IRangeOperationTypeEnum.Delete,
rowColType: IRowColTypeEnum.Col,
index: range.startColumn,
count: insertColCount,
},
},
},
});
}
});
return { undos, redos };
}
private _generateTableMutationWithRemoveRow(removeParams: IRemoveRowColCommandParams) {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const preRedos: IMutationInfo[] = [];
const preUndos: IMutationInfo[] = [];
const target = getSheetCommandTarget(this._univerInstanceService);
if (!target) {
return { undos, redos, preRedos, preUndos };
}
const { unitId, subUnitId } = target;
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (!allSubUnitTables.length) {
return { undos, redos, preRedos, preUndos };
}
const { range } = removeParams;
const removeRowCount = range.endRow - range.startRow + 1;
allSubUnitTables.forEach((table) => {
const tableRange = table.getRange();
if (Rectangle.intersects(tableRange, range)) {
if (range.startRow <= tableRange.startRow && range.endRow >= tableRange.startRow) {
preRedos.push({
id: DeleteSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
},
});
const tableJson = table.toJSON();
undos.push({
id: AddSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: tableJson.id,
name: tableJson.name,
range: tableJson.range,
options: tableJson.options,
},
});
} else if (range.startRow > tableRange.startRow && range.startRow <= tableRange.endRow) {
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
endRow: tableRange.endRow - removeRowCount,
},
},
},
},
});
undos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
},
},
},
},
});
} else if (range.startRow < tableRange.endRow && range.endRow >= tableRange.endRow) {
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
endRow: range.startRow - 1,
},
},
},
},
});
undos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
updateRange: {
newRange: {
...tableRange,
},
},
},
},
});
}
}
});
return { undos, redos, preRedos, preUndos };
}
private _generateTableMutationWithRemoveCol(removeParams: IRemoveRowColCommandParams) {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const preRedos: IMutationInfo[] = [];
const preUndos: IMutationInfo[] = [];
const target = getSheetCommandTarget(this._univerInstanceService);
if (!target) {
return { undos, redos, preRedos, preUndos };
}
const { unitId, subUnitId } = target;
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (!allSubUnitTables.length) {
return { undos, redos, preRedos, preUndos };
}
const { range } = removeParams;
const removeColCount = range.endColumn - range.startColumn + 1;
allSubUnitTables.forEach((table) => {
const tableRange = table.getRange();
if (Rectangle.intersects(tableRange, range)) {
if (range.startColumn <= tableRange.startColumn && range.endColumn >= tableRange.endColumn) {
const tableInfo = table.getTableInfo();
const formulaMutations = this._sheetInterceptorService.onCommandExecute({
id: DELETE_SHEET_TABLE_COMMAND_ID,
params: {
unitId,
subUnitId,
tableId: table.getId(),
tableName: tableInfo.name,
},
});
preRedos.push(...(formulaMutations.preRedos ?? []), ...formulaMutations.redos);
preRedos.push({
id: DeleteSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
},
});
const tableJson = table.toJSON();
const { startRow, startColumn, endColumn } = tableJson.range;
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
const worksheet = workbook?.getSheetBySheetId(subUnitId);
if (!worksheet) {
return { undos, redos, preRedos, preUndos };
}
const header: string[] = [];
for (let i = startColumn; i <= endColumn; i++) {
header.push(convertCellDataToString(worksheet?.getCell(startRow, i)) || getColumnName(i - startColumn + 1, this._localeService.t<LocaleKey>('sheets-table.columnPrefix')));
}
undos.push({
id: AddSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: tableJson.id,
name: tableJson.name,
header,
range: tableJson.range,
options: tableJson.options,
},
});
undos.push(...(formulaMutations.preUndos ?? []), ...formulaMutations.undos);
} else if (range.startColumn <= tableRange.startColumn && range.endColumn >= tableRange.startColumn) {
const tableJson = table.toJSON();
const removeColumnCount = range.endColumn - tableRange.startColumn + 1;
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
rowColOperation: {
operationType: IRangeOperationTypeEnum.Delete,
rowColType: IRowColTypeEnum.Col,
index: tableRange.startColumn,
count: removeColumnCount,
},
},
},
});
const columns: ITableColumnJson[] = [];
for (let i = 0; i < removeColumnCount; i++) {
const column = table.getTableColumnByIndex(i);
if (column) {
columns.push(column.toJSON());
}
}
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
this._appendTableColumnFormulaMutations(redos, undos, {
unitId,
subUnitId,
tableId: table.getId(),
tableName: table.getTableInfo().name,
range,
columns,
});
} else if (range.startColumn > tableRange.startColumn && range.endColumn > tableRange.endColumn) {
const tableJson = table.toJSON();
const removeColumnCount = tableRange.endColumn - range.startColumn + 1;
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
rowColOperation: {
operationType: IRangeOperationTypeEnum.Delete,
rowColType: IRowColTypeEnum.Col,
index: range.startColumn,
count: removeColumnCount,
},
},
},
});
const columns: ITableColumnJson[] = [];
const gap = range.startColumn - tableRange.startColumn;
for (let i = 0; i < removeColumnCount; i++) {
const column = table.getTableColumnByIndex(i + gap);
if (column) {
columns.push(column.toJSON());
}
}
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
this._appendTableColumnFormulaMutations(redos, undos, {
unitId,
subUnitId,
tableId: table.getId(),
tableName: table.getTableInfo().name,
range,
columns,
});
} else if (range.startColumn > tableRange.startColumn && range.endColumn <= tableRange.endColumn) {
const tableJson = table.toJSON();
redos.push({
id: SetSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: table.getId(),
config: {
rowColOperation: {
operationType: IRangeOperationTypeEnum.Delete,
rowColType: IRowColTypeEnum.Col,
index: range.startColumn,
count: removeColCount,
},
},
},
});
const columns: ITableColumnJson[] = [];
const gap = range.startColumn - tableRange.startColumn;
for (let i = 0; i < removeColCount; i++) {
const column = table.getTableColumnByIndex(i + gap);
if (column) {
columns.push(column.toJSON());
}
}
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
this._appendTableColumnFormulaMutations(redos, undos, {
unitId,
subUnitId,
tableId: table.getId(),
tableName: table.getTableInfo().name,
range,
columns,
});
}
}
});
return { undos, redos, preRedos, preUndos };
}
private _getDeleteTableMutation(unitId: string, subUnitId: string, tableId: string): IMutationInfo {
return {
id: DeleteSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId,
},
};
}
private _getAddTableMutation(unitId: string, subUnitId: string, tableJson: ITableJson): IMutationInfo {
const header = tableJson.columns.map((column) => column.displayName);
return {
id: AddSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: tableJson.id,
name: tableJson.name,
header,
range: tableJson.range,
options: {
...tableJson.options,
columns: tableJson.columns,
filters: tableJson.filters.tableColumnFilterList,
},
},
};
}
private _appendTableColumnFormulaMutations(
redos: IMutationInfo[],
undos: IMutationInfo[],
info: {
unitId: string;
subUnitId: string;
tableId: string;
tableName: string;
range: IRemoveRowColCommandParams['range'];
columns: ITableColumnJson[];
}
) {
const removedColumnNames = info.columns.map((column) => column.displayName);
if (!removedColumnNames.length) {
return;
}
const formulaMutations = this._sheetInterceptorService.onCommandExecute({
id: SHEET_TABLE_REMOVE_COL_COMMAND_ID,
params: {
unitId: info.unitId,
subUnitId: info.subUnitId,
tableId: info.tableId,
tableName: info.tableName,
range: info.range,
removedColumnNames,
},
});
redos.splice(Math.max(redos.length - 1, 0), 0, ...(formulaMutations.preRedos ?? []), ...formulaMutations.redos);
undos.push(...(formulaMutations.preUndos ?? []), ...formulaMutations.undos);
}
private _initCommandListener() {
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === InsertRowMutation.id) {
const params = commandInfo.params as IInsertRowMutationParams;
const { unitId, subUnitId, range } = params;
const insertCount = range.endRow - range.startRow + 1;
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
allTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startRow <= tableRange.startRow) {
this._tableManager.updateTableRange(unitId, table.getId(), {
newRange: {
...tableRange,
startRow: tableRange.startRow + insertCount,
endRow: tableRange.endRow + insertCount,
},
});
}
});
} else if (commandInfo.id === InsertColMutation.id) {
const params = commandInfo.params as IInsertRowMutationParams;
const { unitId, subUnitId, range } = params;
const insertCount = range.endColumn - range.startColumn + 1;
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
allTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startColumn <= tableRange.startColumn) {
this._tableManager.updateTableRange(unitId, table.getId(), {
newRange: {
...tableRange,
startColumn: tableRange.startColumn + insertCount,
endColumn: tableRange.endColumn + insertCount,
},
});
}
});
} else if (commandInfo.id === RemoveRowMutation.id) {
const params = commandInfo.params as IInsertRowMutationParams;
const { unitId, subUnitId, range } = params;
const removeCount = range.endRow - range.startRow + 1;
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
allTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startRow < tableRange.startRow) {
this._tableManager.updateTableRange(unitId, table.getId(), {
newRange: {
...tableRange,
startRow: tableRange.startRow - removeCount,
endRow: tableRange.endRow - removeCount,
},
});
}
});
} else if (commandInfo.id === RemoveColMutation.id) {
const params = commandInfo.params as IInsertRowMutationParams;
const { unitId, subUnitId, range } = params;
const removeCount = range.endColumn - range.startColumn + 1;
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
allTables.forEach((table) => {
const tableRange = table.getRange();
if (range.startColumn < tableRange.startColumn) {
this._tableManager.updateTableRange(unitId, table.getId(), {
newRange: {
...tableRange,
startColumn: tableRange.startColumn - removeCount,
endColumn: tableRange.endColumn - removeCount,
},
});
}
});
}
});
}
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ITableDefaultThemeStyle, IUniverSheetsTableConfig } from '../config/config';
import { Disposable, IConfigService, Inject } from '@univerjs/core';
import { RangeThemeStyle, SheetRangeThemeModel, SheetRangeThemeService } from '@univerjs/sheets';
import { SHEETS_TABLE_PLUGIN_CONFIG_KEY } from '../config/config';
import { TableManager } from '../models/table-manager';
import { tableThemeConfig } from './table-theme.factory';
export class SheetsTableThemeController extends Disposable {
private _defaultThemeIndex: number = 0;
private _allThemes: ITableDefaultThemeStyle[] = [];
constructor(
@Inject(TableManager) private _tableManager: TableManager,
@Inject(SheetRangeThemeService) private _sheetRangeThemeService: SheetRangeThemeService,
@Inject(SheetRangeThemeModel) private _sheetRangeThemeModel: SheetRangeThemeModel,
@IConfigService private readonly _configService: IConfigService
) {
super();
this._initUserTableTheme();
this.registerTableChangeEvent();
this._initDefaultTableTheme();
}
registerTableChangeEvent() {
this.disposeWithMe(
this._tableManager.tableAdd$.subscribe((event) => {
const { range, tableId, unitId, subUnitId, tableStyleId } = event;
const table = this._tableManager.getTable(unitId, tableId)!;
// If the tableStyleId is not set or is empty string, use the default theme
const _tableStyleId = tableStyleId || this._allThemes[this._defaultThemeIndex].name;
table.setTableStyleId(_tableStyleId);
this._sheetRangeThemeService.registerRangeThemeStyle(_tableStyleId, {
unitId,
subUnitId,
range: { ...range },
});
})
);
this.disposeWithMe(
this._tableManager.tableRangeChanged$.subscribe((event) => {
const { range, oldRange, tableId, unitId, subUnitId } = event;
const table = this._tableManager.getTable(unitId, tableId)!;
let tableStyleId = table.getTableStyleId();
if (!tableStyleId) {
tableStyleId = this._allThemes[this._defaultThemeIndex].name;
table.setTableStyleId(tableStyleId);
}
this._sheetRangeThemeService.removeRangeThemeRule(tableStyleId, {
unitId,
subUnitId,
range: { ...oldRange },
});
this._sheetRangeThemeService.registerRangeThemeStyle(tableStyleId, {
unitId,
subUnitId,
range: { ...range },
});
})
);
this.disposeWithMe(
this._tableManager.tableThemeChanged$.subscribe((event) => {
const { theme, oldTheme, tableId, unitId, subUnitId } = event;
const table = this._tableManager.getTable(unitId, tableId)!;
const range = table.getRange();
this._sheetRangeThemeService.removeRangeThemeRule(oldTheme, {
unitId,
subUnitId,
range: { ...range },
});
this._sheetRangeThemeService.registerRangeThemeStyle(theme, {
unitId,
subUnitId,
range: { ...range },
});
})
);
this.disposeWithMe(
this._tableManager.tableDelete$.subscribe((event) => {
const { range, unitId, subUnitId, tableStyleId = this._allThemes[this._defaultThemeIndex].name } = event;
this._sheetRangeThemeService.removeRangeThemeRule(tableStyleId, {
unitId,
subUnitId,
range: { ...range },
});
})
);
}
private _initUserTableTheme() {
const tableConfig: IUniverSheetsTableConfig = this._configService.getConfig(SHEETS_TABLE_PLUGIN_CONFIG_KEY) || {};
const defaultThemeIndex = tableConfig.defaultThemeIndex || 0;
const userThemes = tableConfig.userThemes || [];
this._defaultThemeIndex = defaultThemeIndex;
this._allThemes = userThemes.concat(tableThemeConfig);
}
private _initDefaultTableTheme() {
for (let i = 0; i < this._allThemes.length; i++) {
const { name, style } = this._allThemes[i];
const rangeThemeStyle = new RangeThemeStyle(name, style);
this._sheetRangeThemeModel.registerDefaultRangeTheme(rangeThemeStyle);
}
}
override dispose() {
super.dispose();
this._allThemes = [];
this._defaultThemeIndex = 0;
}
}

View File

@@ -0,0 +1,299 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IMutationInfo, IRange, Workbook } from '@univerjs/core';
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
import type { ITableResource } from '../types/type';
import { Disposable, generateRandomId, Inject, InterceptorEffectEnum, IResourceManagerService, IUniverInstanceService, Rectangle, RTree, UniverInstanceType } from '@univerjs/core';
import { CopySheetCommand, INTERCEPTOR_POINT, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import { AddSheetTableMutation } from '../commands/mutations/add-sheet-table.mutation';
import { DeleteSheetTableMutation } from '../commands/mutations/delete-sheet-table.mutation';
import { PLUGIN_NAME } from '../const';
import { TableManager } from '../models/table-manager';
export class SheetsTableController extends Disposable {
private _tableRangeRTree = new Map<string, RTree>();
constructor(
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@Inject(TableManager) private _tableManager: TableManager,
@Inject(IResourceManagerService) private _resourceManagerService: IResourceManagerService
) {
super();
this._initSnapshot();
this._initSheetChange();
this.registerTableChangeEvent();
this.registerTableHeaderInterceptor();
}
getContainerTableWithRange(unitId: string, subUnitId: string, range: IRange) {
const rTree = this._ensureTableRangeRTree(unitId);
const ids = Array.from(rTree.bulkSearch([{ unitId, sheetId: subUnitId, range }]));
const wrapperTableId = ids.find((id) => {
const table = this._tableManager.getTable(unitId, String(id));
if (table) {
return Rectangle.contains(table.getRange(), range);
}
return false;
});
if (wrapperTableId) {
const table = this._tableManager.getTable(unitId, String(wrapperTableId));
return table;
}
}
private _ensureTableRangeRTree(unitId: string) {
if (!this._tableRangeRTree.has(unitId)) {
this._tableRangeRTree.set(unitId, new RTree());
}
return this._tableRangeRTree.get(unitId)!;
}
registerTableChangeEvent() {
this.disposeWithMe(
this._tableManager.tableAdd$.subscribe((event) => {
const { range, tableId, unitId, subUnitId } = event;
const rTree = this._ensureTableRangeRTree(unitId);
rTree.insert({
unitId,
sheetId: subUnitId,
id: tableId,
range: { ...range },
});
})
);
this.disposeWithMe(
this._tableManager.tableRangeChanged$.subscribe((event) => {
const { range, tableId, unitId, subUnitId, oldRange } = event;
const rTree = this._ensureTableRangeRTree(unitId);
rTree.remove({
unitId,
sheetId: subUnitId,
id: tableId,
range: { ...oldRange },
});
rTree.insert({
unitId,
sheetId: subUnitId,
id: tableId,
range: { ...range },
});
})
);
this.disposeWithMe(
this._tableManager.tableDelete$.subscribe((event) => {
const { tableId, unitId, subUnitId, range } = event;
const rTree = this._ensureTableRangeRTree(unitId);
rTree.remove({
unitId,
sheetId: subUnitId,
id: tableId,
range: { ...range },
});
})
);
}
registerTableHeaderInterceptor() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, {
effect: InterceptorEffectEnum.Value,
handler: (cell, context, next) => {
const { row, col, unitId, subUnitId } = context;
const rTree = this._ensureTableRangeRTree(unitId);
const v = cell?.v;
if (v === undefined && rTree) {
const ids = Array.from(rTree.bulkSearch([{ unitId, sheetId: subUnitId, range: { startColumn: col, endColumn: col, startRow: row, endRow: row } }]));
if (ids.length > 0) {
const table = this._tableManager.getTable(unitId, ids[0] as string);
if (table) {
const tableRange = table.getRange();
const index = col - tableRange.startColumn;
const tableStartRow = tableRange.startRow;
if (tableStartRow === row) {
const columnName = table.getColumnNameByIndex(index);
if (!cell || cell === context.rawData) {
cell = { ...context.rawData };
}
cell.v = columnName;
return next(cell);
}
}
}
}
return next(cell);
},
})
);
}
private _toJson(unitId: string): ITableResource {
return this._tableManager.toJSON(unitId);
}
private _fromJSON(unitId: string, resources: ITableResource) {
return this._tableManager.fromJSON(unitId, resources);
}
private _deleteUnitId(unitId: string) {
this._tableManager.deleteUnitId(unitId);
}
private _initSnapshot(): void {
this.disposeWithMe(this._resourceManagerService.registerPluginResource({
toJson: (unitId: string) => {
return JSON.stringify(this._toJson(unitId));
},
parseJson: (json: string) => {
if (!json) {
return {};
}
try {
return JSON.parse(json);
// eslint-disable-next-line unused-imports/no-unused-vars
} catch (error) {
return {};
}
},
businesses: [UniverInstanceType.UNIVER_SHEET],
pluginName: PLUGIN_NAME,
onLoad: (unitId, resources) => {
this._fromJSON(unitId, resources);
},
onUnLoad: (unitId) => {
this._deleteUnitId(unitId);
},
}));
}
// eslint-disable-next-line max-lines-per-function
private _initSheetChange() {
this.disposeWithMe(
this._sheetInterceptorService.interceptCommand({
// eslint-disable-next-line max-lines-per-function
getMutations: (commandInfo) => {
if (commandInfo.id === RemoveSheetCommand.id) {
const params = commandInfo.params as IRemoveSheetCommandParams;
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
const subUnitId = params.subUnitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
if (!unitId || !subUnitId) {
return { redos: [], undos: [] };
}
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (tables.length === 0) {
return { redos: [], undos: [] };
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
tables.forEach((table) => {
const tableJson = table.toJSON();
redos.push({
id: DeleteSheetTableMutation.id,
params: {
unitId,
subUnitId,
tableId: tableJson.id,
},
});
undos.push({
id: AddSheetTableMutation.id,
params: {
unitId,
subUnitId,
name: tableJson.name,
range: tableJson.range,
tableId: tableJson.id,
options: {
...tableJson.options,
columns: tableJson.columns,
filters: tableJson.filters.tableColumnFilterList,
},
},
});
});
return { redos, undos };
} else if (commandInfo.id === CopySheetCommand.id) {
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
const { unitId, subUnitId, targetSubUnitId } = params;
if (!unitId || !subUnitId || !targetSubUnitId) {
return { redos: [], undos: [] };
}
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
if (tables.length === 0) {
return { redos: [], undos: [] };
}
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
tables.forEach((table) => {
const tableJson = table.toJSON();
const tableId = generateRandomId();
redos.push({
id: AddSheetTableMutation.id,
params: {
unitId,
subUnitId: targetSubUnitId,
name: tableJson.name,
range: {
...tableJson.range,
sheetId: targetSubUnitId,
},
tableId,
options: {
...tableJson.options,
columns: tableJson.columns,
filters: tableJson.filters.tableColumnFilterList,
},
},
});
undos.push({
id: DeleteSheetTableMutation.id,
params: {
unitId,
subUnitId: targetSubUnitId,
tableId,
},
});
});
return { redos, undos };
}
return { redos: [], undos: [] };
},
})
);
}
override dispose() {
super.dispose();
this._tableRangeRTree.clear();
}
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Workbook } from '@univerjs/core';
import type { Subscription } from 'rxjs';
import { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { getSheetCommandTarget, INTERCEPTOR_POINT, SheetInterceptorService, ZebraCrossingCacheController } from '@univerjs/sheets';
import { filter, switchMap } from 'rxjs';
import { TableManager } from '../models/table-manager';
export class TableFilterController extends Disposable {
private readonly _tableFilteredOutRows = new Map<string, Set<number>>();
private _subscription: Subscription | null = null;
constructor(
@Inject(TableManager) private _tableManager: TableManager,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
@Inject(ZebraCrossingCacheController) private readonly _zebraCrossingCacheController: ZebraCrossingCacheController
) {
super();
this.registerFilterChangeEvent();
this.initTableHiddenRowIntercept();
this._initFilteredOutRows();
}
initTableHiddenRowIntercept() {
this.disposeWithMe(this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.ROW_FILTERED, {
// Priority must be higher than sheet filtering, because filtering has no next, and those below filtering will not trigger interceptor
priority: 100,
handler: (filtered, rowLocation, next) => {
if (filtered) return true;
const isTableFiltered = this._getTableFilteredOutRows(rowLocation.unitId, rowLocation.subUnitId).has(rowLocation.row);
return isTableFiltered ? true : next(isTableFiltered);
},
}));
}
private _initFilteredOutRows() {
this._tableManager.tableInitStatus$.pipe(
filter((initialized) => initialized),
switchMap(() => {
return this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
}),
filter((workbook) => workbook !== null && workbook !== undefined),
switchMap((workbook) => workbook.activeSheet$),
filter((sheet) => sheet !== null && sheet !== undefined)
).subscribe(() => {
const target = getSheetCommandTarget(this._univerInstanceService);
if (!target) {
return;
}
const { unitId, subUnitId } = target;
this._refreshTableFilteredOutRows(unitId, subUnitId);
});
}
registerFilterChangeEvent() {
this.disposeWithMe(
this._tableManager.tableFilterChanged$.subscribe((event) => {
const { unitId, subUnitId, tableId } = event;
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
const worksheet = workbook?.getSheetBySheetId(subUnitId);
const table = this._tableManager.getTable(unitId, tableId);
if (!worksheet || !table) {
return;
}
const tableFilter = table.getTableFilters();
tableFilter.doFilter(worksheet, table.getTableFilterRange(), workbook?.getDateSystem());
this._refreshTableFilteredOutRows(unitId, subUnitId);
this._zebraCrossingCacheController.updateZebraCrossingCache(unitId, subUnitId);
})
);
}
private _refreshTableFilteredOutRows(unitId: string, subUnitId: string): void {
const filteredOutRows = new Set<number>();
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
tables.forEach((table) => {
const tableFilteredRows = table.getTableFilters().getFilterOutRows();
if (!tableFilteredRows) {
return;
}
for (const row of tableFilteredRows) {
filteredOutRows.add(row);
}
});
this._tableFilteredOutRows.set(this._getSheetKey(unitId, subUnitId), filteredOutRows);
}
private _getTableFilteredOutRows(unitId: string, subUnitId: string): Set<number> {
return this._tableFilteredOutRows.get(this._getSheetKey(unitId, subUnitId)) ?? new Set();
}
private _getSheetKey(unitId: string, subUnitId: string): string {
return `${unitId}|${subUnitId}`;
}
override dispose(): void {
super.dispose();
this._subscription?.unsubscribe();
}
}

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRangeThemeStyleItem, IRangeThemeStyleJSON } from '@univerjs/sheets';
import type { ITableDefaultThemeStyle } from '../config/config';
import { tableDefaultBorderStyle } from '../config/config';
export const customEmptyThemeWithBorderStyle: Omit<IRangeThemeStyleJSON, 'name'> = {
headerRowStyle: {
bd: {
t: tableDefaultBorderStyle,
},
},
headerColumnStyle: {
bd: {
l: tableDefaultBorderStyle,
},
},
lastColumnStyle: {
bd: {
r: tableDefaultBorderStyle,
},
},
lastRowStyle: {
bd: {
b: tableDefaultBorderStyle,
},
},
};
export const processStyleWithBorderStyle = (key: keyof Omit<IRangeThemeStyleJSON, 'name'>, style: IRangeThemeStyleItem) => {
if (key === 'headerRowStyle') {
if (!style.bd) {
return {
...style,
bd: {
t: tableDefaultBorderStyle,
},
};
}
} else if (key === 'lastRowStyle') {
if (!style.bd) {
return {
...style,
bd: {
b: tableDefaultBorderStyle,
},
};
}
} else if (key === 'lastColumnStyle') {
if (!style.bd) {
return {
...style,
bd: {
r: tableDefaultBorderStyle,
},
};
}
} else if (key === 'headerColumnStyle') {
if (!style.bd) {
return {
...style,
bd: {
l: tableDefaultBorderStyle,
},
};
}
}
return style;
};
const tableDefaultThemeStyleArr = [
[['#6280F9', '#FFFFFF', '#EEF2FF', '#DCE4FF'], ['#fff']],
[['#16BDCA', '#FFFFFF', '#EDFAFA', '#AFECEF'], ['#000']],
[['#31C48D', '#FFFFFF', '#F3FAF7', '#BCF0DA'], ['#fff']],
[['#AC94FA', '#FFFFFF', '#F6F5FF', '#EDEBFE'], ['#fff']],
[['#F17EBB', '#FFFFFF', '#FDF2F8', '#FCE8F3'], ['#fff']],
[['#F98080', '#FFFFFF', '#FDF2F2', '#FDE8E8'], ['#fff']],
];
export const tableThemeConfig: ITableDefaultThemeStyle[] = tableDefaultThemeStyleArr.map((item, index) => {
const [backgroundArr, colorArr] = item;
const [headerRowBg, firstRowBg, secondRowBg, lastRowBg] = backgroundArr;
const [headerCl] = colorArr;
return {
name: `table-default-${index}`,
style: {
headerRowStyle: {
bg: {
rgb: headerRowBg,
},
cl: {
rgb: headerCl,
},
bd: {
t: tableDefaultBorderStyle,
},
},
headerColumnStyle: {
bd: {
l: tableDefaultBorderStyle,
},
},
firstRowStyle: {
bg: {
rgb: firstRowBg,
},
},
secondRowStyle: {
bg: {
rgb: secondRowBg,
},
},
lastRowStyle: {
bg: {
rgb: lastRowBg,
},
bd: {
b: tableDefaultBorderStyle,
},
},
lastColumnStyle: {
bd: {
r: tableDefaultBorderStyle,
},
},
},
};
});