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
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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]]);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
299
packages/sheets-table/src/controllers/sheets-table.controller.ts
Normal file
299
packages/sheets-table/src/controllers/sheets-table.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
120
packages/sheets-table/src/controllers/table-filter.controller.ts
Normal file
120
packages/sheets-table/src/controllers/table-filter.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
142
packages/sheets-table/src/controllers/table-theme.factory.ts
Normal file
142
packages/sheets-table/src/controllers/table-theme.factory.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user