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

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

View File

@@ -0,0 +1,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,
};
}

View File

@@ -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();
});
});

View File

@@ -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);
});
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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: [] });
});
});

View File

@@ -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();
});
});

View File

@@ -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!');
});
});
});

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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,
}
));
}
}

View File

@@ -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,
};
}
}

View File

@@ -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();
});
}
}

View File

@@ -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
));
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 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');
});
});

View File

@@ -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),
};

View File

@@ -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',
};

View 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));
}
}

View File

@@ -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);
});
});

View 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;
}