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,188 @@
|
||||
/**
|
||||
* 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, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { EditorService, IEditorService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
CalculateFormulaService,
|
||||
DefinedNamesService,
|
||||
FormulaCurrentConfigService,
|
||||
FormulaDataModel,
|
||||
FormulaRuntimeService,
|
||||
HyperlinkEngineFormulaService,
|
||||
ICalculateFormulaService,
|
||||
IDefinedNamesService,
|
||||
IFormulaCurrentConfigService,
|
||||
IFormulaRuntimeService,
|
||||
IHyperlinkEngineFormulaService,
|
||||
LexerTreeBuilder,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
DefinedNameDataController,
|
||||
IRefSelectionsService,
|
||||
RangeProtectionRuleModel,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
WorkbookPermissionService,
|
||||
WorksheetPermissionService,
|
||||
WorksheetProtectionPointModel,
|
||||
WorksheetProtectionRuleModel,
|
||||
} from '@univerjs/sheets';
|
||||
import { EditorBridgeService, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
},
|
||||
1: {
|
||||
f: '=SUM(A1)',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
1: {
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
9: {
|
||||
0: {
|
||||
f: '=B10',
|
||||
},
|
||||
},
|
||||
19: {
|
||||
0: {
|
||||
v: 20,
|
||||
},
|
||||
},
|
||||
20: {
|
||||
0: {
|
||||
f: '=B21',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
has: Injector['has'];
|
||||
sheet: Workbook;
|
||||
}
|
||||
|
||||
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
const has = injector.has.bind(injector);
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
private _formulaDataModel: FormulaDataModel | null = null;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([WorksheetPermissionService]);
|
||||
injector.add([WorksheetProtectionPointModel]);
|
||||
injector.add([RangeProtectionRuleModel]);
|
||||
injector.add([WorkbookPermissionService]);
|
||||
injector.add([WorksheetProtectionRuleModel]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([ICalculateFormulaService, { useClass: CalculateFormulaService }]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([DefinedNameDataController]);
|
||||
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
|
||||
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
|
||||
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
|
||||
const hasSheetSkeletonManagerService = dependencies?.some((dependency) =>
|
||||
Array.isArray(dependency) && dependency[0] === SheetSkeletonManagerService
|
||||
);
|
||||
if (!hasSheetSkeletonManagerService) {
|
||||
injector.add([SheetSkeletonManagerService]);
|
||||
}
|
||||
injector.add([IEditorBridgeService, { useClass: EditorBridgeService }]);
|
||||
injector.add([IEditorService, { useClass: EditorService }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([IRefSelectionsService, { useClass: SheetsSelectionsService }]);
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
this._injector.get(WorkbookPermissionService);
|
||||
this._injector.get(WorksheetPermissionService);
|
||||
this._injector.get(DefinedNameDataController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._formulaDataModel = get(FormulaDataModel);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData || TEST_WORKBOOK_DATA_DEMO);
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
has,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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 { ErrorType, extractFormulaError } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaAlertRenderController } from '../formula-alert-render.controller';
|
||||
|
||||
vi.mock('@univerjs/engine-formula', async (importActual) => {
|
||||
const actual = await importActual<typeof import('@univerjs/engine-formula')>();
|
||||
return {
|
||||
...actual,
|
||||
extractFormulaError: vi.fn(() => actual.ErrorType.DIV_BY_ZERO),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('FormulaAlertRenderController', () => {
|
||||
it('shows formula alerts for hovered cells and hides them for repeated hovers', async () => {
|
||||
vi.useFakeTimers();
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const currentAlert = new Map<string, { alert: { location: { unitId: string; subUnitId: string; row: number; col: number } } }>();
|
||||
const showAlert = vi.fn((alert) => currentAlert.set('SHEET_FORMULA_ALERT', { alert }));
|
||||
const removeAlert = vi.fn((key: string) => currentAlert.delete(key));
|
||||
const worksheet = { getCell: vi.fn(() => ({ v: '#DIV/0!' })) };
|
||||
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: vi.fn(() => worksheet) } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{ currentAlert, removeAlert, showAlert } as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: vi.fn(() => null) } as never
|
||||
);
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
|
||||
const location = { unitId: 'book-1', subUnitId: 'sheet-1', row: 2, col: 1 };
|
||||
currentCell$.next({ location });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(showAlert).toHaveBeenCalledWith(expect.objectContaining({
|
||||
key: 'SHEET_FORMULA_ALERT',
|
||||
title: 'sheets-formula-ui.error.title',
|
||||
message: 'sheets-formula-ui.error.divByZero',
|
||||
location,
|
||||
}));
|
||||
|
||||
currentCell$.next({ location });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
expect(removeAlert).toHaveBeenCalledWith('SHEET_FORMULA_ALERT');
|
||||
});
|
||||
|
||||
it('hides alerts when there is no extractable formula error', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(null as never);
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const removeAlert = vi.fn();
|
||||
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: vi.fn(() => ({ getCell: vi.fn(() => ({ v: 'plain-text' })) })) } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{ currentAlert: new Map(), removeAlert, showAlert: vi.fn() } as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: vi.fn(() => null) } as never
|
||||
);
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
|
||||
currentCell$.next({ location: { unitId: 'book-1', subUnitId: 'sheet-1', row: 0, col: 0 } });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(removeAlert).toHaveBeenCalledWith('SHEET_FORMULA_ALERT');
|
||||
expect(vi.mocked(extractFormulaError)).toHaveBeenCalledWith({ v: 'plain-text' }, false);
|
||||
expect(ErrorType.DIV_BY_ZERO).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides alerts when the workbook has no active worksheet for the hovered cell', async () => {
|
||||
vi.useFakeTimers();
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const removedAlerts: string[] = [];
|
||||
const shownAlerts: unknown[] = [];
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: () => null } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{
|
||||
currentAlert: new Map(),
|
||||
removeAlert: (key: string) => {
|
||||
removedAlerts.push(key);
|
||||
},
|
||||
showAlert: (alert: unknown) => {
|
||||
shownAlerts.push(alert);
|
||||
},
|
||||
} as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: () => null } as never
|
||||
);
|
||||
|
||||
currentCell$.next({ location: { unitId: 'book-1', subUnitId: 'sheet-1', row: 3, col: 4 } });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(removedAlerts).toEqual(['SHEET_FORMULA_ALERT']);
|
||||
expect(shownAlerts).toEqual([]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* 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 { ICellData, Injector, Nullable, Univer, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
set,
|
||||
ThemeService,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeMutation,
|
||||
AutoFillCommand,
|
||||
AutoFillController,
|
||||
AutoFillService,
|
||||
IAutoFillService,
|
||||
RangeProtectionRenderModel,
|
||||
RangeProtectionService,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { FormulaAutoFillController } from '@univerjs/sheets-formula';
|
||||
import {
|
||||
AutoFillUIController,
|
||||
ISheetSelectionRenderService,
|
||||
SheetSelectionRenderService,
|
||||
SheetsRenderService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { IPlatformService, IShortcutService, PlatformService, ShortcutService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
class mockSheetsRenderService {
|
||||
registerSkeletonChangingMutations(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('Test auto fill with formula', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let themeService: ThemeService;
|
||||
let getValues: (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
) => Array<Array<Nullable<ICellData>>> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[ISheetSelectionRenderService, { useClass: SheetSelectionRenderService }],
|
||||
[IAutoFillService, { useClass: AutoFillService }],
|
||||
[IShortcutService, { useClass: ShortcutService }],
|
||||
[IPlatformService, { useClass: PlatformService }],
|
||||
[SheetsRenderService, { useClass: mockSheetsRenderService }],
|
||||
[AutoFillController],
|
||||
[AutoFillUIController],
|
||||
[FormulaAutoFillController],
|
||||
[RangeProtectionService],
|
||||
[RangeProtectionRenderModel],
|
||||
]);
|
||||
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
themeService = get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
get(AutoFillController);
|
||||
get(AutoFillUIController);
|
||||
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(AutoFillCommand);
|
||||
|
||||
get(FormulaAutoFillController);
|
||||
|
||||
getValues = (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
): Array<Array<Nullable<ICellData>>> | undefined =>
|
||||
get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(startRow, startColumn, endRow, endColumn)
|
||||
.getValues();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('correct situations', () => {
|
||||
it('one cell with formula', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
const sourceRange = {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
};
|
||||
|
||||
const targetRange = {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
};
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, { sourceRange, targetRange });
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { f: '=SUM(A1)' } ],
|
||||
// [ { f: '=SUM(A2)', si: '1ZMZWH' } ],
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// ]
|
||||
let values = getValues(0, 1, 2, 1);
|
||||
let B2 = values && values[1][0];
|
||||
let B3 = values && values[2][0];
|
||||
|
||||
expect(B2?.f).toStrictEqual('=SUM(A2)');
|
||||
expect(B2?.si).toEqual(B3?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(0, 1, 2, 1);
|
||||
B2 = values && values[1][0];
|
||||
B3 = values && values[2][0];
|
||||
|
||||
expect(B2).toStrictEqual({
|
||||
t: 2,
|
||||
v: 1,
|
||||
});
|
||||
expect(B3).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(0, 1, 2, 1);
|
||||
B2 = values && values[1][0];
|
||||
B3 = values && values[2][0];
|
||||
|
||||
expect(B2?.f).toStrictEqual('=SUM(A2)');
|
||||
expect(B2?.si).toEqual(B3?.si);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:D3 values will be in the following format
|
||||
// [
|
||||
// [
|
||||
// {
|
||||
// "f": "=SUM(A1)",
|
||||
// },
|
||||
// {
|
||||
// "f": "=SUM(B1)",
|
||||
// "si": "tF5X2Q"
|
||||
// },
|
||||
// {
|
||||
// "si": "tF5X2Q"
|
||||
// }
|
||||
// ],
|
||||
// [
|
||||
// {
|
||||
// "f": "=SUM(A2)",
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// }
|
||||
// ],
|
||||
// [
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
values = getValues(0, 1, 2, 3);
|
||||
let C1 = values && values[0][1];
|
||||
let C2 = values && values[1][1];
|
||||
let C3 = values && values[2][1];
|
||||
let D1 = values && values[0][2];
|
||||
let D2 = values && values[1][2];
|
||||
let D3 = values && values[2][2];
|
||||
|
||||
expect(C1?.f).toStrictEqual('=SUM(B1)');
|
||||
expect(C2?.f).toBeUndefined();
|
||||
expect(C1?.si).toEqual(D1?.si);
|
||||
|
||||
expect(B2?.si).toEqual(C2?.si);
|
||||
expect(D2?.si).toEqual(C2?.si);
|
||||
|
||||
expect(B3?.si).toEqual(C3?.si);
|
||||
expect(D3?.si).toEqual(C3?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(0, 1, 2, 3);
|
||||
C1 = values && values[0][1];
|
||||
C2 = values && values[1][1];
|
||||
C3 = values && values[2][1];
|
||||
D1 = values && values[0][2];
|
||||
D2 = values && values[1][2];
|
||||
D3 = values && values[2][2];
|
||||
|
||||
expect(C1).toBeNull();
|
||||
expect(C2).toBeNull();
|
||||
expect(C3).toBeNull();
|
||||
expect(D1).toBeNull();
|
||||
expect(D2).toBeNull();
|
||||
expect(D3).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(0, 1, 2, 3);
|
||||
C1 = values && values[0][1];
|
||||
C2 = values && values[1][1];
|
||||
C3 = values && values[2][1];
|
||||
D1 = values && values[0][2];
|
||||
D2 = values && values[1][2];
|
||||
D3 = values && values[2][2];
|
||||
|
||||
expect(C1?.f).toStrictEqual('=SUM(B1)');
|
||||
expect(C2?.f).toBeUndefined();
|
||||
expect(C1?.si).toEqual(D1?.si);
|
||||
|
||||
expect(B2?.si).toEqual(C2?.si);
|
||||
expect(D2?.si).toEqual(C2?.si);
|
||||
|
||||
expect(B3?.si).toEqual(C3?.si);
|
||||
expect(D3?.si).toEqual(C3?.si);
|
||||
|
||||
// Restore the original data
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fill up with formula', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 9, startColumn: 0, endRow: 9, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 9,
|
||||
endRow: 9,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 6,
|
||||
endRow: 9,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { f: '=B7', si: '1ZMZWH' } ],
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { f: '=B10' } ],
|
||||
// ]
|
||||
let values = getValues(6, 0, 8, 0);
|
||||
let A7 = values && values[0][0];
|
||||
let A8 = values && values[1][0];
|
||||
let A9 = values && values[2][0];
|
||||
|
||||
expect(A7?.f).toStrictEqual('=B7');
|
||||
expect(A7?.si).toEqual(A8?.si);
|
||||
expect(A7?.si).toEqual(A9?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(6, 0, 8, 0);
|
||||
A7 = values && values[0][0];
|
||||
A8 = values && values[1][0];
|
||||
A9 = values && values[2][0];
|
||||
|
||||
expect(A7).toBeNull();
|
||||
expect(A8).toBeNull();
|
||||
expect(A9).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(6, 0, 8, 0);
|
||||
A7 = values && values[0][0];
|
||||
A8 = values && values[1][0];
|
||||
A9 = values && values[2][0];
|
||||
|
||||
expect(A7?.f).toStrictEqual('=B7');
|
||||
expect(A7?.si).toEqual(A8?.si);
|
||||
expect(A7?.si).toEqual(A9?.si);
|
||||
});
|
||||
|
||||
it('fill up with formula and number', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 19, startColumn: 0, endRow: 20, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 19,
|
||||
endRow: 20,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 15,
|
||||
endRow: 20,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { v: 18 } ]
|
||||
// [ { f: '=B17', si: '1ZMZWH' } ],
|
||||
// [ { v: 19 } ]
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { v: 20 } ]
|
||||
// [ { f: '=B21' } ],
|
||||
// ]
|
||||
let values = getValues(15, 0, 18, 0);
|
||||
let A16 = values && values[0][0];
|
||||
let A17 = values && values[1][0];
|
||||
let A18 = values && values[2][0];
|
||||
let A19 = values && values[3][0];
|
||||
|
||||
expect(A16?.v).toStrictEqual(18);
|
||||
expect(A17?.f).toEqual('=B17');
|
||||
expect(A18?.v).toEqual(19);
|
||||
expect(A19?.si).toEqual(A17?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(15, 0, 18, 0);
|
||||
A16 = values && values[0][0];
|
||||
A17 = values && values[1][0];
|
||||
A18 = values && values[2][0];
|
||||
A19 = values && values[3][0];
|
||||
|
||||
expect(A16).toBeNull();
|
||||
expect(A17).toBeNull();
|
||||
expect(A18).toBeNull();
|
||||
expect(A19).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(15, 0, 18, 0);
|
||||
A16 = values && values[0][0];
|
||||
A17 = values && values[1][0];
|
||||
A18 = values && values[2][0];
|
||||
A19 = values && values[3][0];
|
||||
|
||||
expect(A16?.v).toStrictEqual(18);
|
||||
expect(A17?.f).toEqual('=B17');
|
||||
expect(A18?.v).toEqual(19);
|
||||
expect(A19?.si).toEqual(A17?.si);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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 { Injector, Univer } from '@univerjs/core';
|
||||
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import {
|
||||
BEFORE_CELL_EDIT,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaEditorShowController } from '../formula-editor-show.controller';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
type EditLocation = NonNullable<ReturnType<IEditorBridgeService['getEditLocation']>>;
|
||||
|
||||
let activeUniver: Univer | null = null;
|
||||
|
||||
function createEditLocation(isInArrayFormulaRange = false): EditLocation {
|
||||
return {
|
||||
unitId: 'test',
|
||||
sheetId: 'sheet1',
|
||||
row: 1,
|
||||
column: 1,
|
||||
isInArrayFormulaRange,
|
||||
} as EditLocation;
|
||||
}
|
||||
|
||||
function createControllerTestBed(editLocation = createEditLocation()) {
|
||||
let injector: Injector;
|
||||
const renderContext = {
|
||||
unitId: 'test',
|
||||
scene: {
|
||||
onTransformChange$: {
|
||||
subscribeEvent: () => ({ dispose: vi.fn() }),
|
||||
},
|
||||
},
|
||||
};
|
||||
const createSheetSkeletonManagerService = () =>
|
||||
injector.createInstance(SheetSkeletonManagerService, renderContext as never);
|
||||
const createFormulaEditorShowController = () =>
|
||||
injector.createInstance(FormulaEditorShowController, renderContext as never);
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[
|
||||
SheetSkeletonManagerService,
|
||||
{ useFactory: createSheetSkeletonManagerService },
|
||||
],
|
||||
[
|
||||
FormulaEditorShowController,
|
||||
{ useFactory: createFormulaEditorShowController },
|
||||
],
|
||||
]);
|
||||
activeUniver = testBed.univer;
|
||||
injector = testBed.univer.__getInjector();
|
||||
testBed.sheet.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'Sheet2', cellData: {} });
|
||||
|
||||
const commandService = testBed.get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
|
||||
const editorBridgeService = testBed.get(IEditorBridgeService);
|
||||
vi.spyOn(editorBridgeService, 'getEditLocation').mockReturnValue(editLocation);
|
||||
testBed.get(FormulaEditorShowController);
|
||||
|
||||
return {
|
||||
commandService,
|
||||
editorBridgeService,
|
||||
formulaDataModel: testBed.get(FormulaDataModel),
|
||||
get: testBed.get,
|
||||
injector,
|
||||
refreshEditCellState: vi.spyOn(editorBridgeService, 'refreshEditCellState'),
|
||||
sheet: testBed.sheet,
|
||||
sheetInterceptorService: testBed.get(SheetInterceptorService),
|
||||
};
|
||||
}
|
||||
|
||||
function createSetRangeValuesParams(subUnitId = 'sheet1'): ISetRangeValuesMutationParams {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId,
|
||||
cellValue: {
|
||||
0: {
|
||||
1: { v: 2 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setSpillCellData(formulaDataModel: FormulaDataModel): void {
|
||||
formulaDataModel.setArrayFormulaCellData({
|
||||
test: {
|
||||
sheet1: {
|
||||
1: {
|
||||
1: { v: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
activeUniver?.dispose();
|
||||
activeUniver = null;
|
||||
});
|
||||
|
||||
describe('FormulaEditorShowController', () => {
|
||||
it('preserves the formula string when starting to edit a calculated formula cell', () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const worksheet = testBed.sheet.getSheetBySheetId('sheet1');
|
||||
if (worksheet == null) {
|
||||
throw new Error('Expected sheet1 to exist');
|
||||
}
|
||||
|
||||
const result = testBed.sheetInterceptorService.writeCellInterceptor
|
||||
.fetchThroughInterceptors(BEFORE_CELL_EDIT)(
|
||||
{ v: 3 },
|
||||
{
|
||||
row: 0,
|
||||
col: 1,
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
worksheet,
|
||||
workbook: testBed.sheet,
|
||||
origin: worksheet.getCellRaw(0, 1),
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toEqual({ v: 3, f: '=SUM(A1)' });
|
||||
});
|
||||
|
||||
it('passes an empty edit value through unchanged', () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const worksheet = testBed.sheet.getSheetBySheetId('sheet1');
|
||||
if (worksheet == null) {
|
||||
throw new Error('Expected sheet1 to exist');
|
||||
}
|
||||
|
||||
const result = testBed.sheetInterceptorService.writeCellInterceptor
|
||||
.fetchThroughInterceptors(BEFORE_CELL_EDIT)(
|
||||
null,
|
||||
{
|
||||
row: 0,
|
||||
col: 1,
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
worksheet,
|
||||
workbook: testBed.sheet,
|
||||
origin: worksheet.getCellRaw(0, 1),
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('refreshes when the current cell becomes a spill cell', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('refreshes when the current cell was a spill cell before its array data was removed', async () => {
|
||||
const testBed = createControllerTestBed(createEditLocation(true));
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not refresh for a formula result on a non-spill cell', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh for an ordinary range value mutation', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams()
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh for a formula result on another sheet', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams('sheet2'),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh while the editor is visible', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
testBed.editorBridgeService.changeVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Dblclick,
|
||||
unitId: 'test',
|
||||
});
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes only for the controller associated with the result workbook', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const otherWorkbookController = testBed.injector.createInstance(
|
||||
FormulaEditorShowController,
|
||||
{ unitId: 'other' } as never
|
||||
);
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
otherWorkbookController.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 { ErrorType, extractFormulaError } from '@univerjs/engine-formula';
|
||||
import { INTERCEPTOR_POINT } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaRenderManagerController } from '../formula-render.controller';
|
||||
|
||||
vi.mock('@univerjs/engine-formula', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/engine-formula')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
extractFormulaError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('FormulaRenderManagerController', () => {
|
||||
it('marks formula error cells without mutating raw cell data', () => {
|
||||
let interceptor: any;
|
||||
const disposable = { dispose: vi.fn() };
|
||||
const sheetInterceptorService = {
|
||||
intercept: vi.fn((point, config) => {
|
||||
interceptor = config;
|
||||
return disposable;
|
||||
}),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getArrayFormulaCellData: vi.fn(() => ({ unit: { sheet: { 1: { 2: { f: '=A1' } } } } })),
|
||||
};
|
||||
const controller = new FormulaRenderManagerController(sheetInterceptorService as any, formulaDataModel as any);
|
||||
const rawCell = { v: '#DIV/0!', markers: { br: { size: 1 } } };
|
||||
vi.mocked(extractFormulaError).mockReturnValue(ErrorType.DIV_BY_ZERO);
|
||||
|
||||
const result = interceptor.handler(rawCell, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 1,
|
||||
col: 2,
|
||||
rawData: rawCell,
|
||||
}, (cell: unknown) => cell);
|
||||
|
||||
expect(sheetInterceptorService.intercept).toHaveBeenCalledWith(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
expect.objectContaining({ priority: 10 })
|
||||
);
|
||||
expect(result).not.toBe(rawCell);
|
||||
expect(result.markers).toEqual(expect.objectContaining({
|
||||
br: { size: 1 },
|
||||
tl: { size: 6, color: '#409f11' },
|
||||
}));
|
||||
expect(extractFormulaError).toHaveBeenCalledWith(rawCell, true);
|
||||
|
||||
controller.dispose();
|
||||
expect(disposable.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes through cells when there is no formula error or cell data', () => {
|
||||
let interceptor: any;
|
||||
const controller = new FormulaRenderManagerController({
|
||||
intercept: vi.fn((_point, config) => {
|
||||
interceptor = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any, {
|
||||
getArrayFormulaCellData: vi.fn(() => null),
|
||||
} as any);
|
||||
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(null as never);
|
||||
expect(interceptor.handler({ v: 'plain' }, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 0,
|
||||
col: 0,
|
||||
rawData: { v: 'plain' },
|
||||
}, (cell: unknown) => cell)).toEqual({ v: 'plain' });
|
||||
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(ErrorType.VALUE);
|
||||
expect(interceptor.handler(null, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 0,
|
||||
col: 0,
|
||||
rawData: null,
|
||||
}, (cell: unknown) => cell)).toBeNull();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 { IWorkbookData } from '@univerjs/core';
|
||||
import { LocaleType } from '@univerjs/core';
|
||||
import { ReorderRangeCommand, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FormulaReorderController } from '../formula-reorder.controller';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'unit',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'Test workbook',
|
||||
styles: {},
|
||||
sheetOrder: ['sheet'],
|
||||
sheets: {
|
||||
sheet: {
|
||||
id: 'sheet',
|
||||
name: 'Sheet 1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'old-row' } },
|
||||
1: { 0: { f: '=A2', si: 'shared' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('FormulaReorderController', () => {
|
||||
let univer: ReturnType<typeof createCommandTestBed>['univer'];
|
||||
let controller: FormulaReorderController;
|
||||
let sheetInterceptorService: SheetInterceptorService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(WORKBOOK_DATA, [[FormulaReorderController]]);
|
||||
univer = testBed.univer;
|
||||
controller = testBed.get(FormulaReorderController);
|
||||
sheetInterceptorService = testBed.get(SheetInterceptorService);
|
||||
});
|
||||
|
||||
afterEach(() => univer.dispose());
|
||||
|
||||
it('creates redo and undo mutations with formula references shifted to the reordered row', () => {
|
||||
const result = sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
order: { 0: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.redos).toEqual([{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
cellValue: {
|
||||
0: {
|
||||
0: { f: '=A1', si: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
expect(result.undos).toEqual([{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
cellValue: {
|
||||
0: {
|
||||
0: { v: 'old-row' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
controller.dispose();
|
||||
expect(sheetInterceptorService.onCommandExecute({ id: ReorderRangeCommand.id, params: {} })).toEqual({
|
||||
preUndos: [],
|
||||
undos: [],
|
||||
preRedos: [],
|
||||
redos: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty mutations for non-reorder commands, missing worksheets and ranges without formulas', () => {
|
||||
expect(sheetInterceptorService.onCommandExecute({ id: 'other-command', params: {} })).toMatchObject({ redos: [], undos: [] });
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: { unitId: 'unit', subUnitId: 'missing', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, order: {} },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: { unitId: 'unit', subUnitId: 'sheet', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, order: {} },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 { UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ImageFormulaRenderController } from '../image-formula-render.controller';
|
||||
|
||||
describe('ImageFormulaRenderController', () => {
|
||||
it('refreshes sheet skeleton and marks the main component dirty for image formula changes', () => {
|
||||
let refresh: () => void = () => undefined;
|
||||
const skeletonManagerService = { reCalculate: vi.fn() };
|
||||
const mainComponent = { makeDirty: vi.fn() };
|
||||
const render = {
|
||||
with: vi.fn((token) => token === SheetSkeletonManagerService ? skeletonManagerService : null),
|
||||
mainComponent,
|
||||
};
|
||||
const workbook = { getUnitId: vi.fn(() => 'unit-1') };
|
||||
|
||||
new ImageFormulaRenderController(
|
||||
{ registerRefreshRenderFunction: vi.fn((fn) => { refresh = fn; }) } as any,
|
||||
{ getRenderUnitById: vi.fn(() => render) } as any,
|
||||
{ getCurrentUnitOfType: vi.fn((type) => type === UniverInstanceType.UNIVER_SHEET ? workbook : null) } as any
|
||||
);
|
||||
|
||||
refresh();
|
||||
|
||||
expect(render.with).toHaveBeenCalledWith(SheetSkeletonManagerService);
|
||||
expect(skeletonManagerService.reCalculate).toHaveBeenCalled();
|
||||
expect(mainComponent.makeDirty).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when there is no workbook, render, or main component', () => {
|
||||
let refresh: () => void = () => undefined;
|
||||
const imageFormulaController = { registerRefreshRenderFunction: vi.fn((fn) => {
|
||||
refresh = fn;
|
||||
}) };
|
||||
const renderManagerService = { getRenderUnitById: vi.fn<(...args: any[]) => any>(() => null) };
|
||||
const univerInstanceService = { getCurrentUnitOfType: vi.fn<(...args: any[]) => any>(() => null) };
|
||||
|
||||
new ImageFormulaRenderController(
|
||||
imageFormulaController as any,
|
||||
renderManagerService as any,
|
||||
univerInstanceService as any
|
||||
);
|
||||
|
||||
refresh();
|
||||
expect(renderManagerService.getRenderUnitById).not.toHaveBeenCalled();
|
||||
|
||||
univerInstanceService.getCurrentUnitOfType.mockReturnValue({ getUnitId: () => 'unit-1' });
|
||||
refresh();
|
||||
expect(renderManagerService.getRenderUnitById).toHaveBeenCalledWith('unit-1');
|
||||
|
||||
renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
with: vi.fn(() => ({ reCalculate: vi.fn() })),
|
||||
mainComponent: null,
|
||||
});
|
||||
expect(() => refresh()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,763 @@
|
||||
/**
|
||||
* 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 { ICellData, Injector, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
|
||||
import type { IDeleteRangeMoveLeftCommandParams, IDeleteRangeMoveUpCommandParams, IInsertColCommandParams, IInsertRangeMoveDownCommandParams, IInsertRangeMoveRightCommandParams, IInsertRowCommandParams, IMoveColsCommandParams, IMoveRangeCommandParams, IMoveRowsCommandParams, IRemoveRowColCommandParams, IRemoveSheetCommandParams, ISetWorksheetNameCommandParams } from '@univerjs/sheets';
|
||||
import { Direction, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
|
||||
import { IDefinedNamesService, RemoveDefinedNameMutation, SetArrayFormulaDataMutation, SetDefinedNameMutation, SetFormulaDataMutation } from '@univerjs/engine-formula';
|
||||
import { DeleteRangeMoveLeftCommand, DeleteRangeMoveUpCommand, InsertColByRangeCommand, InsertColCommand, InsertColMutation, InsertRangeMoveDownCommand, InsertRangeMoveRightCommand, InsertRowByRangeCommand, InsertRowCommand, InsertRowMutation, MoveColsCommand, MoveColsMutation, MoveRangeCommand, MoveRangeMutation, MoveRowsCommand, MoveRowsMutation, RemoveColByRangeCommand, RemoveColCommand, RemoveColMutation, RemoveRowByRangeCommand, RemoveRowCommand, RemoveRowMutation, RemoveSheetCommand, RemoveSheetMutation, SetDefinedNameCommand, SetRangeValuesCommand, SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetNameCommand, SetWorksheetNameMutation, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { UpdateDefinedNameController } from '@univerjs/sheets-formula';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => ({
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {},
|
||||
name: 'Sheet1',
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
cellData: {},
|
||||
name: 'Sheet2',
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_DEFINED_NAME_PLUGIN',
|
||||
data: '{"soAI3OK4sq":{"id":"soAI3OK4sq","name":"DefinedName1","formulaOrRefString":"Sheet1!$A$1:$B$2","comment":"","localSheetId":"AllDefaultWorkbook"},"qwert12345":{"id":"qwert12345","name":"DefinedName2","formulaOrRefString":"Sheet1!$1:$3","comment":"","localSheetId":"AllDefaultWorkbook"},"asdfg67890":{"id":"asdfg67890","name":"DefinedName3","formulaOrRefString":"Sheet1!$A:$C","comment":"","localSheetId":"AllDefaultWorkbook"}}',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('Test update defined name', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let getValues: (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
sheetId?: string
|
||||
) => Array<Array<Nullable<ICellData>>> | undefined;
|
||||
let getDefinedNameRef: (name?: string) => string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA_DEMO(), [
|
||||
[UpdateDefinedNameController],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
get(UpdateDefinedNameController);
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(MoveRangeCommand);
|
||||
commandService.registerCommand(MoveRangeMutation);
|
||||
|
||||
commandService.registerCommand(MoveRowsCommand);
|
||||
commandService.registerCommand(MoveRowsMutation);
|
||||
|
||||
commandService.registerCommand(MoveColsCommand);
|
||||
commandService.registerCommand(MoveColsMutation);
|
||||
|
||||
commandService.registerCommand(InsertRowCommand);
|
||||
commandService.registerCommand(InsertRowMutation);
|
||||
|
||||
commandService.registerCommand(InsertColCommand);
|
||||
commandService.registerCommand(InsertColMutation);
|
||||
|
||||
commandService.registerCommand(RemoveRowCommand);
|
||||
commandService.registerCommand(RemoveRowMutation);
|
||||
|
||||
commandService.registerCommand(RemoveColCommand);
|
||||
commandService.registerCommand(RemoveColMutation);
|
||||
|
||||
commandService.registerCommand(DeleteRangeMoveLeftCommand);
|
||||
commandService.registerCommand(DeleteRangeMoveUpCommand);
|
||||
commandService.registerCommand(InsertRangeMoveDownCommand);
|
||||
commandService.registerCommand(InsertRangeMoveRightCommand);
|
||||
|
||||
commandService.registerCommand(SetWorksheetNameCommand);
|
||||
commandService.registerCommand(SetWorksheetNameMutation);
|
||||
commandService.registerCommand(RemoveSheetCommand);
|
||||
commandService.registerCommand(RemoveSheetMutation);
|
||||
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(SetRangeValuesCommand);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetFormulaDataMutation);
|
||||
commandService.registerCommand(SetArrayFormulaDataMutation);
|
||||
|
||||
// set defined name
|
||||
commandService.registerCommand(SetDefinedNameCommand);
|
||||
commandService.registerCommand(SetDefinedNameMutation);
|
||||
commandService.registerCommand(RemoveDefinedNameMutation);
|
||||
|
||||
[
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertColByRangeCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
|
||||
getValues = (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
sheetId: string = 'sheet1'
|
||||
): Array<Array<Nullable<ICellData>>> | undefined =>
|
||||
get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId(sheetId)
|
||||
?.getRange(startRow, startColumn, endRow, endColumn)
|
||||
.getValues();
|
||||
|
||||
getDefinedNameRef = (name: string = 'DefinedName1') => {
|
||||
const definedNameService = get(IDefinedNamesService);
|
||||
const definedName = definedNameService.getValueByName('test', name);
|
||||
if (definedName) {
|
||||
return definedName.formulaOrRefString;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('update defined name', () => {
|
||||
it('Move range, update reference', async () => {
|
||||
const params: IMoveRangeCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 3,
|
||||
endRow: 1,
|
||||
endColumn: 4,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$D$1:$E$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$D$1:$E$2');
|
||||
});
|
||||
|
||||
it('Move rows, update reference', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 4,
|
||||
startColumn: 0,
|
||||
endRow: 4,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$4');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$4');
|
||||
});
|
||||
|
||||
it('Move rows to range start', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$2:$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$2:$3');
|
||||
});
|
||||
|
||||
it('Move rows to range end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 3,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
});
|
||||
|
||||
it('Move first row beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 9,
|
||||
startColumn: 0,
|
||||
endRow: 9,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
});
|
||||
|
||||
it('Move last row beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 2, startColumn: 0, endRow: 2, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 2,
|
||||
startColumn: 0,
|
||||
endRow: 2,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 9,
|
||||
startColumn: 0,
|
||||
endRow: 9,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$9');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$9');
|
||||
});
|
||||
|
||||
it('Move columns, update reference', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 4,
|
||||
endRow: 999,
|
||||
endColumn: 4,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Move columns to range start', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 999,
|
||||
endColumn: 1,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$B:$C');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$B:$C');
|
||||
});
|
||||
|
||||
it('Move columns to range end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 3,
|
||||
endRow: 999,
|
||||
endColumn: 3,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
});
|
||||
|
||||
it('Move first column beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 9,
|
||||
endRow: 999,
|
||||
endColumn: 9,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
});
|
||||
|
||||
it('Move last column beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 2, endRow: 0, endColumn: 2, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 2,
|
||||
endRow: 999,
|
||||
endColumn: 2,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 9,
|
||||
endRow: 999,
|
||||
endColumn: 9,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$I');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$I');
|
||||
});
|
||||
|
||||
it('Insert row, update reference', async () => {
|
||||
const params: IInsertRowCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: {
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
},
|
||||
direction: Direction.UP,
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRowCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
});
|
||||
|
||||
it('Insert column, update reference', async () => {
|
||||
const params: IInsertColCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 14,
|
||||
},
|
||||
direction: Direction.LEFT,
|
||||
cellValue: {},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertColCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
});
|
||||
|
||||
it('Remove row, update reference', async () => {
|
||||
const params: IRemoveRowColCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveRowCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
});
|
||||
|
||||
it('Remove column, update reference', async () => {
|
||||
const params: IRemoveRowColCommandParams = {
|
||||
range: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Delete move left, update reference', async () => {
|
||||
const params: IDeleteRangeMoveLeftCommandParams = {
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Delete move up, update reference', async () => {
|
||||
const params: IDeleteRangeMoveUpCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
});
|
||||
|
||||
it('Insert move down, update reference', async () => {
|
||||
const params: IInsertRangeMoveDownCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
});
|
||||
|
||||
it('Insert move right, update reference', async () => {
|
||||
const params: IInsertRangeMoveRightCommandParams = {
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
});
|
||||
|
||||
it('set worksheet name', async () => {
|
||||
const params: ISetWorksheetNameCommandParams = {
|
||||
subUnitId: 'sheet1',
|
||||
name: 'Sheet1Rename',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(SetWorksheetNameCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1Rename!$A$1:$B$2');
|
||||
});
|
||||
|
||||
it('remove worksheet', async () => {
|
||||
const params: IRemoveSheetCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveSheetCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('#REF!');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 {
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
} from '@univerjs/icons';
|
||||
import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, RANGE_SELECTOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { FormulaEditor } from '../views/formula-editor/index';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../views/more-functions/interface';
|
||||
import { MoreFunctions } from '../views/more-functions/MoreFunctions';
|
||||
import { RangeSelector } from '../views/range-selector';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(MORE_FUNCTIONS_COMPONENT, MoreFunctions));
|
||||
|
||||
this.disposeWithMe(this._componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, RangeSelector));
|
||||
|
||||
this.disposeWithMe(this._componentManager.register(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, FormulaEditor));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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 { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { Disposable, Inject, isICellData, LocaleService } from '@univerjs/core';
|
||||
import { ErrorType, extractFormulaError, FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { CellAlertManagerService, CellAlertType, HoverManagerService } from '@univerjs/sheets-ui';
|
||||
import { debounceTime } from 'rxjs';
|
||||
|
||||
const ALERT_KEY = 'SHEET_FORMULA_ALERT';
|
||||
|
||||
const ErrorTypeToMessageMap = {
|
||||
[ErrorType.DIV_BY_ZERO]: 'divByZero',
|
||||
[ErrorType.NAME]: 'name',
|
||||
[ErrorType.VALUE]: 'value',
|
||||
[ErrorType.NUM]: 'num',
|
||||
[ErrorType.NA]: 'na',
|
||||
[ErrorType.CYCLE]: 'cycle',
|
||||
[ErrorType.REF]: 'ref',
|
||||
[ErrorType.SPILL]: 'spill',
|
||||
[ErrorType.CALC]: 'calc',
|
||||
[ErrorType.ERROR]: 'error',
|
||||
[ErrorType.CONNECT]: 'connect',
|
||||
[ErrorType.NULL]: 'null',
|
||||
};
|
||||
|
||||
export class FormulaAlertRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
|
||||
@Inject(CellAlertManagerService) private readonly _cellAlertManagerService: CellAlertManagerService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initCellAlertPopup();
|
||||
}
|
||||
|
||||
private _initCellAlertPopup() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentCell$.pipe(debounceTime(100)).subscribe((cellPos) => {
|
||||
if (cellPos) {
|
||||
const workbook = this._context.unit;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
|
||||
if (!worksheet) return this._hideAlert();
|
||||
|
||||
const cellData = worksheet.getCell(cellPos.location.row, cellPos.location.col);
|
||||
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData()?.
|
||||
[cellPos.location.unitId]?.
|
||||
[cellPos.location.subUnitId]?.
|
||||
[cellPos.location.row]?.
|
||||
[cellPos.location.col];
|
||||
|
||||
// Preventing blank object
|
||||
if (isICellData(cellData)) {
|
||||
const errorType = extractFormulaError(cellData, !!arrayFormulaCellData);
|
||||
|
||||
if (!errorType) {
|
||||
// fix #4002
|
||||
this._hideAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAlert = this._cellAlertManagerService.currentAlert.get(ALERT_KEY);
|
||||
const currentLoc = currentAlert?.alert?.location;
|
||||
if (
|
||||
currentLoc &&
|
||||
currentLoc.row === cellPos.location.row &&
|
||||
currentLoc.col === cellPos.location.col &&
|
||||
currentLoc.subUnitId === cellPos.location.subUnitId &&
|
||||
currentLoc.unitId === cellPos.location.unitId
|
||||
) {
|
||||
this._hideAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
this._cellAlertManagerService.showAlert({
|
||||
type: CellAlertType.ERROR,
|
||||
title: this._localeService.t<LocaleKey>('sheets-formula-ui.error.title'),
|
||||
message: this._localeService.t(`sheets-formula-ui.error.${ErrorTypeToMessageMap[errorType]}`),
|
||||
location: cellPos.location,
|
||||
width: 200,
|
||||
height: 74,
|
||||
key: ALERT_KEY,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._hideAlert();
|
||||
}));
|
||||
}
|
||||
|
||||
private _hideAlert() {
|
||||
this._cellAlertManagerService.removeAlert(ALERT_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICellData, ICellDataWithSpanAndDisplay, IMutationInfo, IRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IDiscreteRange, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import type { ICellDataWithSpanInfo, ICopyPastePayload, IPasteHookValueType, ISheetClipboardHook, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
DEFAULT_EMPTY_DOCUMENT_VALUE,
|
||||
Disposable,
|
||||
generateRandomId,
|
||||
getEmptyCell,
|
||||
Inject,
|
||||
Injector,
|
||||
isFormulaId,
|
||||
isFormulaString,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '@univerjs/sheets';
|
||||
import { COPY_TYPE, ISheetClipboardService, PREDEFINED_HOOK_NAME_COPY, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
|
||||
export const DEFAULT_PASTE_FORMULA = 'default-paste-formula';
|
||||
|
||||
export class FormulaClipboardController extends Disposable {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder,
|
||||
@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._registerClipboardHook();
|
||||
}
|
||||
|
||||
private _registerClipboardHook() {
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._copyFormulaOnlyHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._pasteFormulaHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._pasteWithFormulaHook()));
|
||||
}
|
||||
|
||||
private _copyFormulaOnlyHook(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
let currentSheet: Nullable<Worksheet> = null;
|
||||
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_COPY.SPECIAL_COPY_FORMULA_ONLY,
|
||||
priority: 10,
|
||||
onBeforeCopy(unitId, subUnitId) {
|
||||
currentSheet = self._getWorksheet(unitId, subUnitId);
|
||||
},
|
||||
onCopyCellContent(row: number, col: number): string {
|
||||
if (!currentSheet) return '';
|
||||
|
||||
const cell = currentSheet.getCellRaw(row, col);
|
||||
if (!cell) return '';
|
||||
|
||||
if (isFormulaString(cell.f)) return cell.f as string;
|
||||
if (isFormulaId(cell.si)) {
|
||||
const formulaDataModel = self._formulaDataModel;
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(row, col, currentSheet.getSheetId(), currentSheet.getUnitId());
|
||||
return formulaString || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
onAfterCopy() {
|
||||
currentSheet = null;
|
||||
},
|
||||
getFilteredOutRows(unitId: string, subUnitId: string, range: IRange) {
|
||||
const worksheet = self._getWorksheet(unitId, subUnitId);
|
||||
if (!worksheet) return [];
|
||||
|
||||
const { startRow, endRow } = range;
|
||||
const res: number[] = [];
|
||||
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
if (worksheet.getRowFiltered(r)) {
|
||||
res.push(r);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
handleMatrixOnCell(
|
||||
row: number,
|
||||
column: number,
|
||||
rowIndexInMatrix: number,
|
||||
columnIndexInMatrix: number,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanAndDisplay>,
|
||||
matrixFragment: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
plainMatrix: ObjectMatrix<ICellDataWithSpanAndDisplay>
|
||||
) {
|
||||
const cellData = matrix.getValue(row, column);
|
||||
if (currentSheet && cellData && (isFormulaString(cellData.f) || isFormulaId(cellData.si))) {
|
||||
const formulaString = isFormulaString(cellData.f)
|
||||
? (cellData.f as string)
|
||||
: self._formulaDataModel.getFormulaStringByCell(row, column, currentSheet.getSheetId(), currentSheet.getUnitId()) as string;
|
||||
matrixFragment.setValue(rowIndexInMatrix, columnIndexInMatrix, {
|
||||
...getEmptyCell(),
|
||||
f: formulaString,
|
||||
});
|
||||
plainMatrix.setValue(rowIndexInMatrix, columnIndexInMatrix, {
|
||||
...getEmptyCell(),
|
||||
f: formulaString,
|
||||
displayV: formulaString,
|
||||
});
|
||||
} else {
|
||||
matrix.setValue(row, column, getEmptyCell());
|
||||
matrixFragment.setValue(rowIndexInMatrix, columnIndexInMatrix, getEmptyCell());
|
||||
plainMatrix.setValue(rowIndexInMatrix, columnIndexInMatrix, getEmptyCell());
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _pasteFormulaHook(): ISheetClipboardHook {
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
priority: 10,
|
||||
specialPasteInfo: { label: 'specialPaste.formula' },
|
||||
onPasteCells: (pasteFrom, pasteTo, data, payload) => this._onPasteCells(pasteFrom, pasteTo, data, payload, true),
|
||||
};
|
||||
}
|
||||
|
||||
private _pasteWithFormulaHook(): ISheetClipboardHook {
|
||||
return {
|
||||
id: DEFAULT_PASTE_FORMULA,
|
||||
priority: 10,
|
||||
onPasteCells: (pasteFrom, pasteTo, data, payload) => this._onPasteCells(pasteFrom, pasteTo, data, payload, false),
|
||||
};
|
||||
}
|
||||
|
||||
private _getWorkbook(unitId?: string): Nullable<Workbook> {
|
||||
if (unitId) {
|
||||
return this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
}
|
||||
|
||||
return this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
}
|
||||
|
||||
private _getWorksheet(unitId?: string, subUnitId?: string): Nullable<Worksheet> {
|
||||
const workbook = this._getWorkbook(unitId);
|
||||
|
||||
if (subUnitId) {
|
||||
return workbook?.getSheetBySheetId(subUnitId);
|
||||
}
|
||||
|
||||
return workbook?.getActiveSheet();
|
||||
}
|
||||
|
||||
private _onPasteCells(
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null,
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
data: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
payload: ICopyPastePayload,
|
||||
isSpecialPaste: boolean
|
||||
) {
|
||||
// Intercept scenarios where formulas do not need to be processed, and only process default paste and paste formulas only
|
||||
const specialPastes: IPasteHookValueType[] = [
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
];
|
||||
if (specialPastes.includes(payload.pasteType)) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (payload.copyType === COPY_TYPE.CUT && !isSpecialPaste) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const workbook = this._getWorkbook();
|
||||
const unitId = pasteTo.unitId || workbook?.getUnitId();
|
||||
const subUnitId = pasteTo.subUnitId || workbook?.getActiveSheet()?.getSheetId();
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const pastedRange = pasteTo.range;
|
||||
const matrix = data;
|
||||
const copyInfo = {
|
||||
copyType: payload.copyType || COPY_TYPE.COPY,
|
||||
copyRange: pasteFrom?.range,
|
||||
pasteType: payload.pasteType,
|
||||
};
|
||||
|
||||
return this._injector.invoke((accessor) => getSetCellFormulaMutations(
|
||||
unitId,
|
||||
subUnitId,
|
||||
pastedRange,
|
||||
matrix,
|
||||
accessor,
|
||||
copyInfo,
|
||||
this._lexerTreeBuilder,
|
||||
this._formulaDataModel,
|
||||
isSpecialPaste,
|
||||
pasteFrom
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export function getSetCellFormulaMutations(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor,
|
||||
copyInfo: {
|
||||
copyType: COPY_TYPE;
|
||||
copyRange?: IDiscreteRange;
|
||||
pasteType: string;
|
||||
},
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
_isSpecialPaste = false,
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null
|
||||
) {
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
|
||||
const valueMatrix = getValueMatrix(unitId, subUnitId, range, matrix, copyInfo, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
|
||||
if (!valueMatrix.hasValue()) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
// set cell value and style
|
||||
const setValuesMutation: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: valueMatrix.clone(),
|
||||
};
|
||||
|
||||
redoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: setValuesMutation,
|
||||
});
|
||||
|
||||
// undo
|
||||
const undoSetValuesMutation: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
|
||||
accessor,
|
||||
setValuesMutation
|
||||
);
|
||||
|
||||
undoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoSetValuesMutation,
|
||||
});
|
||||
return {
|
||||
undos: undoMutationsInfo,
|
||||
redos: redoMutationsInfo,
|
||||
};
|
||||
}
|
||||
|
||||
function getValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
copyInfo: {
|
||||
copyType: COPY_TYPE;
|
||||
copyRange?: IDiscreteRange;
|
||||
pasteType: string;
|
||||
},
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null
|
||||
): ObjectMatrix<ICellData> {
|
||||
if (!pasteFrom) {
|
||||
return getValueMatrixOfPasteFromIsNull(unitId, subUnitId, range, matrix, formulaDataModel);
|
||||
}
|
||||
|
||||
if (copyInfo.pasteType === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE) {
|
||||
return getSpecialPasteValueValueMatrix(unitId, subUnitId, range, matrix, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
if (copyInfo.pasteType === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA) {
|
||||
return getSpecialPasteFormulaValueMatrix(unitId, subUnitId, range, matrix, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
return getDefaultPasteValueMatrix(unitId, subUnitId, range, matrix, copyInfo.copyType, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
function getValueMatrixOfPasteFromIsNull(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
formulaDataModel: FormulaDataModel
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaString(value.v)) {
|
||||
// If the copy value is a formula
|
||||
valueObject.v = null;
|
||||
valueObject.f = `${value.v}`;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getSpecialPasteValueValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const arrayFormulaCellData = formulaDataModel.getArrayFormulaCellData()?.[pasteFrom.unitId]?.[pasteFrom.subUnitId];
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const fromRow = pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const fromCol = pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaString(value.f) || isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (arrayFormulaCellData?.[fromRow]?.[fromCol]) {
|
||||
// If the copy value is an array formula
|
||||
const cell = arrayFormulaCellData[fromRow][fromCol];
|
||||
valueObject.v = cell.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
if (value.p) {
|
||||
const richText = getCellRichText(value);
|
||||
if (richText) {
|
||||
valueObject.v = richText;
|
||||
}
|
||||
}
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getSpecialPasteFormulaValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaIdMap = new Map<string, string>();
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
if (pasteFrom.unitId !== unitId || pasteFrom.subUnitId !== subUnitId) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(formulaString || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = null;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
valueObject.si = value.si;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
// If the copy value is a formula
|
||||
const index = `${row % pasteFrom.range.rows.length}_${col % pasteFrom.range.cols.length}`;
|
||||
let formulaId = formulaIdMap.get(index);
|
||||
|
||||
if (!formulaId) {
|
||||
formulaId = generateRandomId(6);
|
||||
formulaIdMap.set(index, formulaId);
|
||||
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(value.f || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
// At the beginning of the second formula, set formulaId only
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
if (value.p) {
|
||||
const richText = getCellRichText(value);
|
||||
if (richText) {
|
||||
valueObject.v = richText;
|
||||
}
|
||||
}
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function getDefaultPasteValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
copyType: COPY_TYPE,
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaIdMap = new Map<string, string>();
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
const cutFormulaIds: string[] = [];
|
||||
|
||||
if (copyType === COPY_TYPE.CUT) {
|
||||
// If cut, the formula range should not be moved.
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
if (isFormulaString(value.f)) {
|
||||
cutFormulaIds.push(value.si as string);
|
||||
|
||||
valueObject.f = value.f;
|
||||
valueObject.si = value.si;
|
||||
} else if (cutFormulaIds.includes(value.si as string)) {
|
||||
valueObject.f = null;
|
||||
valueObject.si = value.si;
|
||||
} else {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
|
||||
valueObject.f = formulaString;
|
||||
valueObject.si = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
valueObject.f = value.f;
|
||||
valueObject.si = null;
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// copy, the formula range should be moved.
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
if (pasteFrom.unitId !== unitId || pasteFrom.subUnitId !== subUnitId) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(formulaString || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = null;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
valueObject.si = value.si;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
// If the copy value is a formula
|
||||
const index = `${row % pasteFrom.range.rows.length}_${col % pasteFrom.range.cols.length}`;
|
||||
let formulaId = formulaIdMap.get(index);
|
||||
|
||||
if (!formulaId) {
|
||||
formulaId = generateRandomId(6);
|
||||
formulaIdMap.set(index, formulaId);
|
||||
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(value.f || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
// At the beginning of the second formula, set formulaId only
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = value.p;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If cut range has the first formula id, remove the related formula ids and convert it to formula string.
|
||||
if (cutFormulaIds.length > 0) {
|
||||
new ObjectMatrix(formulaData!).forValue((row, col, value) => {
|
||||
if (
|
||||
!(pasteFrom.range.rows.includes(row) && pasteFrom.range.cols.includes(col)) &&
|
||||
!(range.rows.includes(row) && range.cols.includes(col)) &&
|
||||
cutFormulaIds.includes(value?.si as string)
|
||||
) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
row,
|
||||
col,
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
|
||||
valueMatrix.setValue(row, col, {
|
||||
f: formulaString,
|
||||
si: null,
|
||||
v: null,
|
||||
p: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getCellRichText(cell: ICellData) {
|
||||
if (cell?.p) {
|
||||
const body = cell?.p.body;
|
||||
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = body.dataStream;
|
||||
const lastString = data.substring(data.length - 2, data.length);
|
||||
const newDataStream = lastString === DEFAULT_EMPTY_DOCUMENT_VALUE ? data.substring(0, data.length - 2) : data;
|
||||
|
||||
return newDataStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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 {
|
||||
ICellDataForSheetInterceptor,
|
||||
ICommandInfo,
|
||||
IObjectMatrixPrimitiveType,
|
||||
IRange,
|
||||
IRowAutoHeightInfo,
|
||||
Nullable,
|
||||
Workbook,
|
||||
Worksheet,
|
||||
} from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type {
|
||||
ISelectionWithStyle,
|
||||
ISetRangeValuesMutationParams,
|
||||
ISetWorksheetRowAutoHeightMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
ColorKit,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
ObjectMatrix,
|
||||
ThemeService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
ErrorType,
|
||||
FormulaDataModel,
|
||||
SetArrayFormulaDataMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
attachSelectionWithCoord,
|
||||
BEFORE_CELL_EDIT,
|
||||
SetRangeValuesMutation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
IEditorBridgeService,
|
||||
SELECTION_SHAPE_DEPTH,
|
||||
SelectionControl,
|
||||
SheetSkeletonManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
|
||||
/**
|
||||
* For Array formula in cell editing
|
||||
*/
|
||||
export class FormulaEditorShowController extends Disposable implements IRenderModule {
|
||||
private _previousShape: Nullable<SelectionControl>;
|
||||
private _skeleton: SpreadsheetSkeleton;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService
|
||||
) {
|
||||
super();
|
||||
this._initSkeletonChangeListener();
|
||||
this._initInterceptorEditorStart();
|
||||
this._commandExecutedListener();
|
||||
|
||||
// Do not intercept v:null and add t: CellValueType.NUMBER. When the cell =TODAY() is automatically filled, the number format will recognize the Number type and parse it as 1900-01-00 date format.
|
||||
}
|
||||
|
||||
private _initSkeletonChangeListener(): void {
|
||||
this.disposeWithMe(
|
||||
this._sheetSkeletonManagerService.currentSkeleton$.subscribe((param) => {
|
||||
if (param == null) {
|
||||
this._logService.debug('[FormulaEditorShowController]: should not receive currentSkeleton$ as null!');
|
||||
} else {
|
||||
const { skeleton } = param;
|
||||
const prevSheetId = this._skeleton?.worksheet?.getSheetId();
|
||||
this._changeRuntime(skeleton);
|
||||
|
||||
// change to another sheet
|
||||
if (prevSheetId !== skeleton.worksheet.getSheetId()) {
|
||||
this._removeArrayFormulaRangeShape();
|
||||
} else {
|
||||
const { unitId, sheetId } = param;
|
||||
this._updateArrayFormulaRangeShape(unitId, sheetId);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected _changeRuntime(skeleton: SpreadsheetSkeleton): void {
|
||||
this._skeleton = skeleton;
|
||||
}
|
||||
|
||||
private _initInterceptorEditorStart(): void {
|
||||
this.disposeWithMe(
|
||||
toDisposable(
|
||||
this._sheetInterceptorService.writeCellInterceptor.intercept(BEFORE_CELL_EDIT, {
|
||||
handler: (value, context, next) => {
|
||||
const { row, col, unitId, subUnitId, worksheet } = context;
|
||||
const arrayFormulaMatrixRange = this._formulaDataModel.getArrayFormulaRange();
|
||||
|
||||
const arrayFormulaMatrixCell = this._formulaDataModel.getArrayFormulaCellData();
|
||||
|
||||
this._removeArrayFormulaRangeShape();
|
||||
|
||||
if (value == null) {
|
||||
return next(value);
|
||||
}
|
||||
|
||||
let cellInfo: Nullable<ICellDataForSheetInterceptor> = null;
|
||||
|
||||
const formulaString = this._formulaDataModel.getFormulaStringByCell(row, col, subUnitId, unitId);
|
||||
|
||||
if (formulaString !== null) {
|
||||
cellInfo = { f: formulaString };
|
||||
}
|
||||
|
||||
/**
|
||||
* If the display conditions for the array formula are not met, return the range directly.
|
||||
*/
|
||||
if (
|
||||
value.v != null &&
|
||||
value.v !== '' &&
|
||||
arrayFormulaMatrixCell[unitId]?.[subUnitId]?.[row]?.[col] == null
|
||||
) {
|
||||
if (cellInfo) {
|
||||
return { ...value, ...cellInfo };
|
||||
}
|
||||
|
||||
return next(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the array formula for special display in subsequent processing
|
||||
*/
|
||||
const matrixRange = arrayFormulaMatrixRange?.[unitId]?.[subUnitId];
|
||||
if (matrixRange != null) {
|
||||
// For cells other than the upper left corner, the cellInfo information will be updated
|
||||
cellInfo = this._displayArrayFormulaRangeShape(matrixRange, row, col, unitId, subUnitId, worksheet, cellInfo);
|
||||
}
|
||||
|
||||
if (cellInfo) {
|
||||
return { ...value, ...cellInfo };
|
||||
}
|
||||
|
||||
return next(value);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _commandExecutedListener(): void {
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options) => {
|
||||
if (command.id === SetFormulaCalculationResultMutation.id || (command.id === SetArrayFormulaDataMutation.id && options && options.remove)) {
|
||||
this._removeArrayFormulaRangeShape();
|
||||
}
|
||||
|
||||
if (command.id !== SetRangeValuesMutation.id || options?.applyFormulaCalculationResult !== true || command.params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = command.params as ISetRangeValuesMutationParams;
|
||||
const editLocation = this._editorBridgeService.getEditLocation();
|
||||
if (
|
||||
editLocation == null
|
||||
|| params.unitId !== this._context.unitId
|
||||
|| params.unitId !== editLocation.unitId
|
||||
|| params.subUnitId !== editLocation.sheetId
|
||||
|| this._editorBridgeService.isVisible().visible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { row, column, isInArrayFormulaRange } = editLocation;
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData();
|
||||
const isInArrayFormulaCell = arrayFormulaCellData?.[params.unitId]?.[params.subUnitId]?.[row]?.[column] != null;
|
||||
if (isInArrayFormulaRange !== true && !isInArrayFormulaCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editorBridgeService.refreshEditCellState();
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (SetWorksheetRowAutoHeightMutation.id === command.id) {
|
||||
requestIdleCallback(() => {
|
||||
const params = command.params as ISetWorksheetRowAutoHeightMutationParams;
|
||||
const { unitId, subUnitId, rowsAutoHeightInfo } = params;
|
||||
this._refreshArrayFormulaRangeShapeByRow(unitId, subUnitId, rowsAutoHeightInfo);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _displayArrayFormulaRangeShape(matrixRange: IObjectMatrixPrimitiveType<IRange>, row: number, col: number, unitId: string, subUnitId: string, worksheet: Worksheet, cellInfo: Nullable<ICellDataForSheetInterceptor>): Nullable<ICellDataForSheetInterceptor> {
|
||||
// const sheetFormulaData = this._formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
let result = cellInfo;
|
||||
|
||||
new ObjectMatrix(matrixRange).forValue((rowIndex, columnIndex, range) => {
|
||||
if (range == null) {
|
||||
return true;
|
||||
}
|
||||
const { startRow, startColumn, endRow, endColumn } = range;
|
||||
if (rowIndex === row && columnIndex === col) {
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
return false;
|
||||
}
|
||||
if (row >= startRow && row <= endRow && col >= startColumn && col <= endColumn) {
|
||||
const mainCellValue = worksheet.getCell(startRow, startColumn);
|
||||
|
||||
if (mainCellValue?.v === ErrorType.SPILL || mainCellValue?.f == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
result = {
|
||||
f: mainCellValue.f,
|
||||
isInArrayFormulaRange: true,
|
||||
};
|
||||
}
|
||||
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _createArrayFormulaRangeShape(arrayRange: IRange, unitId: string, subUnitId: string): void {
|
||||
const renderUnit = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const skeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!renderUnit || !skeleton) return;
|
||||
|
||||
const { scene } = renderUnit;
|
||||
if (!scene) return;
|
||||
|
||||
const selectionWithStyle: ISelectionWithStyle = {
|
||||
range: arrayRange,
|
||||
primary: null,
|
||||
style: {
|
||||
strokeWidth: 1,
|
||||
stroke: this._themeService.getColorFromTheme('primary.600'),
|
||||
fill: new ColorKit(this._themeService.getColorFromTheme('gray.0')).setAlpha(0).toString(),
|
||||
widgets: {},
|
||||
},
|
||||
};
|
||||
const selectionWithCoord = attachSelectionWithCoord(selectionWithStyle, skeleton);
|
||||
const { rowHeaderWidth, columnHeaderHeight } = skeleton;
|
||||
const control = new SelectionControl(scene, SELECTION_SHAPE_DEPTH.FORMULA_EDITOR_SHOW, this._themeService, {
|
||||
highlightHeader: false,
|
||||
rowHeaderWidth,
|
||||
columnHeaderHeight,
|
||||
});
|
||||
control.updateRangeBySelectionWithCoord(selectionWithCoord);
|
||||
control.setEvent(false);
|
||||
this._previousShape = control;
|
||||
}
|
||||
|
||||
private _removeArrayFormulaRangeShape(): void {
|
||||
if (this._previousShape == null) {
|
||||
return;
|
||||
}
|
||||
this._previousShape.dispose();
|
||||
this._previousShape = null;
|
||||
}
|
||||
|
||||
private _refreshArrayFormulaRangeShape(unitId: string, subUnitId: string): void {
|
||||
if (this._previousShape) {
|
||||
const { startRow, endRow, startColumn, endColumn } = this._previousShape.getRange();
|
||||
const range = { startRow, endRow, startColumn, endColumn };
|
||||
this._removeArrayFormulaRangeShape();
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkCurrentSheet(unitId: string, subUnitId: string): boolean {
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) return false;
|
||||
|
||||
const worksheet = skeleton.worksheet;
|
||||
if (!worksheet) return false;
|
||||
|
||||
if (worksheet.unitId === unitId && worksheet.getSheetId() === subUnitId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _updateArrayFormulaRangeShape(unitId: string, subUnitId: string): void {
|
||||
if (!this._checkCurrentSheet(unitId, subUnitId)) return;
|
||||
if (!this._previousShape) return;
|
||||
this._refreshArrayFormulaRangeShape(unitId, subUnitId);
|
||||
}
|
||||
|
||||
private _refreshArrayFormulaRangeShapeByRow(unitId: string, subUnitId: string, rowAutoHeightInfo: IRowAutoHeightInfo[]): void {
|
||||
if (!this._checkCurrentSheet(unitId, subUnitId)) return;
|
||||
|
||||
if (!this._previousShape) return;
|
||||
|
||||
const { startRow: shapeStartRow } = this._previousShape.getRange();
|
||||
|
||||
for (let i = 0; i < rowAutoHeightInfo.length; i++) {
|
||||
const { row } = rowAutoHeightInfo[i];
|
||||
if (shapeStartRow >= row) {
|
||||
this._refreshArrayFormulaRangeShape(unitId, subUnitId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { Inject, InterceptorEffectEnum, RxDisposable } from '@univerjs/core';
|
||||
import { extractFormulaError, FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
|
||||
|
||||
const FORMULA_ERROR_MARK = {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
};
|
||||
|
||||
export class FormulaRenderManagerController extends RxDisposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._sheetInterceptorService.intercept(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
{
|
||||
effect: InterceptorEffectEnum.Style,
|
||||
handler: (cell, pos, next) => {
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData()?.
|
||||
[pos.unitId]?.
|
||||
[pos.subUnitId]?.
|
||||
[pos.row]?.
|
||||
[pos.col];
|
||||
|
||||
const errorType = extractFormulaError(cell, !!arrayFormulaCellData);
|
||||
if (!errorType) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (!cell) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (cell === pos.rawData) {
|
||||
cell = { ...pos.rawData };
|
||||
}
|
||||
|
||||
cell.markers = {
|
||||
...cell?.markers,
|
||||
...FORMULA_ERROR_MARK,
|
||||
};
|
||||
|
||||
return next(cell);
|
||||
},
|
||||
priority: 10,
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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 { ICellData, IMutationInfo, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IReorderRangeMutationParams } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
Range,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { ReorderRangeCommand, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets';
|
||||
|
||||
export class FormulaReorderController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
|
||||
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
|
||||
getMutations: (command) => {
|
||||
if (command.id === ReorderRangeCommand.id) {
|
||||
return this._reorderFormula(command.params as IReorderRangeMutationParams);
|
||||
}
|
||||
|
||||
return {
|
||||
redos: [],
|
||||
undos: [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _reorderFormula(params: IReorderRangeMutationParams) {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const { unitId, subUnitId, range, order } = params;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (!worksheet) {
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const redoFormulaMatrix = new ObjectMatrix<Nullable<ICellData>>();
|
||||
const undoFormulaMatrix = new ObjectMatrix<Nullable<ICellData>>();
|
||||
|
||||
let hasFormula = false;
|
||||
|
||||
Range.foreach(range, (row, col) => {
|
||||
let targetRow = row;
|
||||
|
||||
if (order.hasOwnProperty(row)) {
|
||||
targetRow = order[row];
|
||||
}
|
||||
|
||||
const targetCell = cellMatrix.getValue(targetRow, col);
|
||||
|
||||
if (targetCell?.f || targetCell?.si) {
|
||||
hasFormula = true;
|
||||
|
||||
const formulaString = this._formulaDataModel.getFormulaStringByCell(targetRow, col, subUnitId, unitId) as string;
|
||||
const shiftedFormula = this._lexerTreeBuilder.moveFormulaRefOffset(
|
||||
formulaString,
|
||||
0,
|
||||
row - targetRow
|
||||
);
|
||||
const newCell = Tools.deepClone(targetCell);
|
||||
newCell.f = shiftedFormula;
|
||||
newCell.si = null;
|
||||
|
||||
redoFormulaMatrix.setValue(row, col, newCell);
|
||||
} else {
|
||||
redoFormulaMatrix.setValue(row, col, targetCell);
|
||||
}
|
||||
|
||||
undoFormulaMatrix.setValue(row, col, cellMatrix.getValue(row, col));
|
||||
});
|
||||
|
||||
if (!hasFormula) {
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
redos.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: redoFormulaMatrix.getMatrix(),
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: undoFormulaMatrix.getMatrix(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ImageFormulaCellInterceptorController } from '@univerjs/sheets-formula';
|
||||
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
|
||||
export class ImageFormulaRenderController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ImageFormulaCellInterceptorController) private readonly _imageFormulaCellInterceptorController: ImageFormulaCellInterceptorController,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._imageFormulaCellInterceptorController.registerRefreshRenderFunction(() => {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return;
|
||||
|
||||
const render = this._renderManagerService.getRenderUnitById(workbook.getUnitId());
|
||||
if (!render) return;
|
||||
|
||||
render.with(SheetSkeletonManagerService).reCalculate();
|
||||
|
||||
const mainComponent = render.mainComponent;
|
||||
if (!mainComponent) return;
|
||||
|
||||
mainComponent.makeDirty();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 {
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
} from '@univerjs/icons';
|
||||
import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, RANGE_SELECTOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { MobileFormulaEditor } from '../../views/mobile/formula-editor/MobileFormulaEditor';
|
||||
import { MobileMoreFunctions } from '../../views/mobile/more-functions/MobileMoreFunctions';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../../views/more-functions/interface';
|
||||
import { MobileRangeSelector } from '../../views/range-selector/MobileRangeSelector';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(MORE_FUNCTIONS_COMPONENT, MobileMoreFunctions));
|
||||
this.disposeWithMe(this._componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, MobileRangeSelector));
|
||||
this.disposeWithMe(this._componentManager.register(
|
||||
EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY,
|
||||
MobileFormulaEditor
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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 type { IInsertFunctionOperationParams } from '../../../commands/operations/insert-function.operation';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ReferenceAbsoluteOperation } from '../../../commands/operations/reference-absolute.operation';
|
||||
|
||||
const whenSheetEditorActivatedMock = vi.fn();
|
||||
const whenSheetEditorFocusedMock = vi.fn();
|
||||
|
||||
vi.mock('@univerjs/sheets-ui', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/sheets-ui')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
whenSheetEditorActivated: whenSheetEditorActivatedMock,
|
||||
whenSheetEditorFocused: whenSheetEditorFocusedMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('formula shortcuts', () => {
|
||||
it('binds F4 to reference absolute switching only while the sheet editor is active', async () => {
|
||||
const { ChangeRefToAbsoluteShortcut } = await import('../prompt.shortcut');
|
||||
const contextService = {};
|
||||
whenSheetEditorActivatedMock.mockReturnValueOnce(true);
|
||||
|
||||
expect(ChangeRefToAbsoluteShortcut.id).toBe(ReferenceAbsoluteOperation.id);
|
||||
expect(ChangeRefToAbsoluteShortcut.binding).toBe(KeyCode.F4);
|
||||
expect(ChangeRefToAbsoluteShortcut.preconditions?.(contextService as never)).toBe(true);
|
||||
expect(whenSheetEditorActivatedMock).toHaveBeenCalledWith(contextService);
|
||||
});
|
||||
|
||||
it('binds quick sum to SUM insertion while the sheet editor is focused', async () => {
|
||||
const { InsertFunctionOperation } = await import('../../../commands/operations/insert-function.operation');
|
||||
const { QuickSumShortcut } = await import('../quick-sum.shortcut');
|
||||
|
||||
expect(QuickSumShortcut.id).toBe(InsertFunctionOperation.id);
|
||||
expect(QuickSumShortcut.staticParameters).toEqual({ value: 'SUM' } satisfies IInsertFunctionOperationParams);
|
||||
expect(QuickSumShortcut.binding).toBe(MetaKeys.ALT | KeyCode.EQUAL);
|
||||
expect(QuickSumShortcut.mac).toBe(MetaKeys.CTRL_COMMAND | MetaKeys.ALT | KeyCode.EQUAL);
|
||||
expect(QuickSumShortcut.preconditions).toBe(whenSheetEditorFocusedMock);
|
||||
expect(QuickSumShortcut.description).toBe('sheets-formula-ui.shortcut.quick-sum');
|
||||
expect(QuickSumShortcut.group).toBe('4_sheet-edit');
|
||||
expect(QuickSumShortcut.groupTitle).toBe('sheets-ui.shortcut.sheet-edit');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import { whenSheetEditorActivated } from '@univerjs/sheets-ui';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { ReferenceAbsoluteOperation } from '../../commands/operations/reference-absolute.operation';
|
||||
|
||||
export const ChangeRefToAbsoluteShortcut: IShortcutItem = {
|
||||
id: ReferenceAbsoluteOperation.id,
|
||||
binding: KeyCode.F4,
|
||||
preconditions: (contextService) => whenSheetEditorActivated(contextService),
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import type { IInsertFunctionOperationParams } from '../../commands/operations/insert-function.operation';
|
||||
import { whenSheetEditorFocused } from '@univerjs/sheets-ui';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import { InsertFunctionOperation } from '../../commands/operations/insert-function.operation';
|
||||
|
||||
export const QuickSumShortcut: IShortcutItem<IInsertFunctionOperationParams> = {
|
||||
id: InsertFunctionOperation.id,
|
||||
binding: MetaKeys.ALT | KeyCode.EQUAL,
|
||||
preconditions: whenSheetEditorFocused,
|
||||
mac: MetaKeys.CTRL_COMMAND | MetaKeys.ALT | KeyCode.EQUAL,
|
||||
staticParameters: { value: 'SUM' },
|
||||
description: 'sheets-formula-ui.shortcut.quick-sum',
|
||||
group: '4_sheet-edit',
|
||||
groupTitle: 'sheets-ui.shortcut.sheet-edit',
|
||||
};
|
||||
88
packages/sheets-formula-ui/src/controllers/ui.controller.ts
Normal file
88
packages/sheets-formula-ui/src/controllers/ui.controller.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { Disposable, ICommandService, Inject, Injector, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetsUIPart } from '@univerjs/sheets-ui';
|
||||
import { connectInjector, IMenuManagerService, IShortcutService, IUIPartsService } from '@univerjs/ui';
|
||||
import { SheetCopyFormulaOnlyCommand, SheetOnlyPasteFormulaCommand } from '../commands/commands/formula-clipboard.command';
|
||||
import { HelpFunctionOperation } from '../commands/operations/help-function.operation';
|
||||
import { InsertFunctionOperation } from '../commands/operations/insert-function.operation';
|
||||
import { MoreFunctionsOperation } from '../commands/operations/more-functions.operation';
|
||||
import { ReferenceAbsoluteOperation } from '../commands/operations/reference-absolute.operation';
|
||||
import { SearchFunctionOperation } from '../commands/operations/search-function.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { FormulaProgressBar } from '../views/formula-progress/FormulaProgress';
|
||||
import { FormulaEditorShowController } from './formula-editor-show.controller';
|
||||
import { ChangeRefToAbsoluteShortcut } from './shortcuts/prompt.shortcut';
|
||||
import { QuickSumShortcut } from './shortcuts/quick-sum.shortcut';
|
||||
|
||||
export class FormulaUIController extends Disposable {
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService,
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._registerCommands();
|
||||
this._registerMenus();
|
||||
this._registerShortcuts();
|
||||
this._registerComponents();
|
||||
this._registerRenderModules();
|
||||
}
|
||||
|
||||
private _registerMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
[
|
||||
SheetCopyFormulaOnlyCommand,
|
||||
SheetOnlyPasteFormulaCommand,
|
||||
InsertFunctionOperation,
|
||||
MoreFunctionsOperation,
|
||||
SearchFunctionOperation,
|
||||
HelpFunctionOperation,
|
||||
ReferenceAbsoluteOperation,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
private _registerShortcuts(): void {
|
||||
[
|
||||
QuickSumShortcut,
|
||||
ChangeRefToAbsoluteShortcut,
|
||||
].forEach((item) => {
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(item));
|
||||
});
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._uiPartsService.registerComponent(SheetsUIPart.FORMULA_AUX, () => connectInjector(FormulaProgressBar, this._injector)));
|
||||
}
|
||||
|
||||
private _registerRenderModules(): void {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [FormulaEditorShowController] as Dependency));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 { CellValueType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractFormulaNumber } from '../utils';
|
||||
|
||||
describe('Test utils', () => {
|
||||
it('Function extractFormulaNumber', () => {
|
||||
const v = 0.07 / 0.1;
|
||||
expect(extractFormulaNumber({ v })).toBeNull();
|
||||
expect(extractFormulaNumber({ v, f: '=SUM(A1)' })).toBeNull();
|
||||
expect(extractFormulaNumber({ v, si: 'id1', f: '=SUM(A1)', t: CellValueType.NUMBER })).toBe(0.7);
|
||||
});
|
||||
});
|
||||
40
packages/sheets-formula-ui/src/controllers/utils/utils.ts
Normal file
40
packages/sheets-formula-ui/src/controllers/utils/utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { ICellData, IContextService, Nullable } from '@univerjs/core';
|
||||
import { CellValueType, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR, isFormulaId, isFormulaString } from '@univerjs/core';
|
||||
import { stripErrorMargin } from '@univerjs/engine-formula';
|
||||
|
||||
export function whenEditorStandalone(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_DOC) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR)
|
||||
// &&
|
||||
// contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the formula number from the cell, handle the precision issue
|
||||
* @param cell
|
||||
* @returns
|
||||
*/
|
||||
export function extractFormulaNumber(cell: Nullable<ICellData>) {
|
||||
if ((isFormulaString(cell?.f) || isFormulaId(cell?.si)) && cell?.t === CellValueType.NUMBER && typeof cell?.v === 'number') {
|
||||
return stripErrorMargin(cell.v);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user