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

40
packages/sheets/README.md Normal file
View File

@@ -0,0 +1,40 @@
# @univerjs/sheets
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets?style=flat-square)](https://npmjs.com/package/@univerjs/sheets)
[![license](https://img.shields.io/npm/l/@univerjs/sheets?style=flat-square)](https://npmjs.com/package/@univerjs/sheets)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets?style=flat-square)](https://npmjs.com/package/@univerjs/sheets)
`@univerjs/sheets` provides the core spreadsheet data model and business logic for Univer Sheets, independent of the UI layer.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets` | `UniverSheets` | No | Yes | Yes |
## Installation
```sh
pnpm add @univerjs/sheets
# or
npm install @univerjs/sheets
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import EnUS from '@univerjs/sheets/locale/en-US';
import { UniverSheetsPlugin } from '@univerjs/sheets';
univer.registerPlugin(UniverSheetsPlugin);
// Merge EnUS into your Univer locale map when this package contributes UI text.
```
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,100 @@
{
"name": "@univerjs/sheets",
"version": "1.0.0-rc.0",
"private": false,
"description": "Core spreadsheet model and business logic for Univer Sheets.",
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [
"univer",
"sheets",
"spreadsheet",
"workbook",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./locale/*": "./src/locale/*.ts",
"./facade": "./src/facade/index.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./locale/*": {
"import": "./lib/es/locale/*.js",
"require": "./lib/cjs/locale/*.js",
"types": "./lib/types/locale/*.d.ts"
},
"./facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"build:bundle": "univer-cli build",
"build:types": "tsc -p tsconfig.node.json",
"build": "pnpm run build:bundle && pnpm run build:types"
},
"peerDependencies": {
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/protocol": "workspace:*",
"@univerjs/rpc": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"@univerjs/network": "workspace:*",
"rxjs": "^7.8.2",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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 } from '@univerjs/core';
import { Styles } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { handleStyle, transformStyle } from '../cell-style';
describe('Test cell style sanitization', () => {
it('ignores unexpected keys when merging cell borders', () => {
const styles = new Styles();
const oldVal: ICellData = {
s: {
bd: {
l: {
s: 0 as never,
cl: { rgb: '#000000' },
},
},
},
};
const newVal: ICellData = {
s: JSON.parse(`{
"bd": {
"__proto__": { "polluted": "yes" },
"evil": { "s": 2, "cl": { "rgb": "#ff00ff" } },
"t": { "s": 1, "cl": { "rgb": "#ff0000" }, "extra": true }
},
"unexpected": { "enabled": true },
"__proto__": { "polluted": "yes" }
}`) as ICellData['s'],
};
handleStyle(styles, oldVal, newVal);
expect(styles.getStyleByCell(oldVal)).toEqual({
bd: {
l: {
s: 0,
cl: { rgb: '#000000' },
},
t: {
s: 1,
cl: { rgb: '#ff0000' },
},
},
});
expect((styles.getStyleByCell(oldVal)?.bd as Record<string, unknown>).evil).toBeUndefined();
expect((styles.getStyleByCell(oldVal) as Record<string, unknown>).unexpected).toBeUndefined();
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
});
it('only records allowed border keys in undo snapshots', () => {
const backupStyle = transformStyle(null, JSON.parse(`{
"bd": {
"t": { "s": 1, "cl": { "rgb": "#00ff00" }, "extra": true },
"evil": { "s": 3, "cl": { "rgb": "#ff00ff" } }
},
"unexpected": { "enabled": true }
}`));
expect(backupStyle).toEqual({
bd: {
t: null,
},
});
});
});

View File

@@ -0,0 +1,45 @@
/**
* 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 { checkCellValueType, isCellImage } from '../cell-type';
describe('test util cell type ', () => {
it('identifies a cell image from its document drawings', () => {
expect(isCellImage(undefined)).toBe(false);
expect(isCellImage({ id: 'doc-1', documentStyle: {}, drawingsOrder: [] })).toBe(false);
expect(isCellImage({ id: 'doc-1', documentStyle: {}, drawingsOrder: ['drawing-1'] })).toBe(true);
});
it('should be able to get the correct type of cell value from "checkCellValueType"', () => {
expect(checkCellValueType('string', CellValueType.BOOLEAN)).toBe(CellValueType.STRING);
expect(checkCellValueType('string', CellValueType.NUMBER)).toBe(CellValueType.STRING);
expect(checkCellValueType('string', CellValueType.STRING)).toBe(CellValueType.STRING);
expect(checkCellValueType('0', CellValueType.STRING)).toBe(CellValueType.NUMBER);
expect(checkCellValueType('true', CellValueType.STRING)).toBe(CellValueType.BOOLEAN);
expect(checkCellValueType('false', CellValueType.STRING)).toBe(CellValueType.BOOLEAN);
expect(checkCellValueType(123, CellValueType.STRING)).toBe(CellValueType.NUMBER);
expect(checkCellValueType(123, CellValueType.BOOLEAN)).toBe(CellValueType.NUMBER); // not a valid boolean number, casted to number
expect(checkCellValueType(123, CellValueType.NUMBER)).toBe(CellValueType.NUMBER);
expect(checkCellValueType(1, CellValueType.NUMBER)).toBe(CellValueType.NUMBER);
expect(checkCellValueType(0, CellValueType.NUMBER)).toBe(CellValueType.NUMBER);
expect(checkCellValueType(1, CellValueType.BOOLEAN)).toBe(CellValueType.BOOLEAN); // it is valid boolean value
expect(checkCellValueType(0, CellValueType.BOOLEAN)).toBe(CellValueType.BOOLEAN); // it is valid boolean value
});
});

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 { describe, expect, it } from 'vitest';
import { extractBooleanValue } from '../cell-value';
describe('test utils cell value', () => {
it('should be able to cast values that can be casted to boolean', () => {
expect(extractBooleanValue(1)).toBe(true);
expect(extractBooleanValue(0)).toBe(false);
expect(extractBooleanValue(-123)).toBe(null);
expect(extractBooleanValue('1')).toBe(true);
expect(extractBooleanValue('0')).toBe(false);
expect(extractBooleanValue('89757')).toBe(null);
expect(extractBooleanValue('true')).toBe(true);
expect(extractBooleanValue('false')).toBe(false);
expect(extractBooleanValue('Michael Jackson')).toBe(null);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
/**
* 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 { createCoreTestBed } from '@univerjs/core/sheets/__tests__/create-core-test-bed.js';
import { beforeEach, describe, expect, it } from 'vitest';
import { expandToContinuousRange } from '../expand-range';
import { snapshot } from './expand.data';
// the work book can be get 'unit=YSvbxFMCTxugbku-IWNyxQ&type=2&subunit=U_wr1DEF84N_mbesFNmxR' in pro
describe('Test expandToContinuousRange', () => {
let workbook: Workbook;
const directions = {
left: true,
right: true,
up: true,
down: true,
};
beforeEach(() => {
const testBed = createCoreTestBed(snapshot);
workbook = testBed.sheet;
});
it('Test expandToContinuousRange all', () => {
const range = {
startRow: 13,
startColumn: 7,
endRow: 13,
endColumn: 7,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(3);
expect(startColumn).toEqual(3);
expect(endColumn).toEqual(17);
expect(endRow).toEqual(21);
});
it('can not expand', () => {
const range = {
startRow: 28,
startColumn: 9,
endRow: 28,
endColumn: 9,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(range.startRow);
expect(startColumn).toEqual(range.startColumn);
expect(endColumn).toEqual(range.endColumn);
expect(endRow).toEqual(range.endRow);
});
it('Test expandToContinuousRange with range', () => {
const range = {
startRow: 8,
startColumn: 7,
endRow: 12,
endColumn: 8,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(3);
expect(startColumn).toEqual(3);
expect(endColumn).toEqual(17);
expect(endRow).toEqual(21);
});
it('expand to right', () => {
const range = {
startRow: 29,
startColumn: 16,
endRow: 29,
endColumn: 16,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(27);
expect(startColumn).toEqual(16);
expect(endColumn).toEqual(19);
expect(endRow).toEqual(29);
});
it('expand to left', () => {
const range = {
startRow: 41,
startColumn: 8,
endRow: 41,
endColumn: 8,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(41);
expect(startColumn).toEqual(5);
expect(endColumn).toEqual(8);
expect(endRow).toEqual(44);
});
it('expand to empty span', () => {
const range = {
startRow: 41,
startColumn: 4,
endRow: 41,
endColumn: 4,
};
const result = expandToContinuousRange(range, directions, workbook.getActiveSheet());
const { startRow, startColumn, endColumn, endRow } = result;
expect(startRow).toEqual(41);
expect(startColumn).toEqual(4);
expect(endColumn).toEqual(7);
expect(endRow).toEqual(43);
});
});

View File

@@ -0,0 +1,215 @@
/**
* 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 { IRange } from '@univerjs/core';
import { cellToRange } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { rangeMerge, RangeMergeUtil } from '../range-merge';
describe('test rangeMerge', () => {
it('simple row', () => {
const testRange: IRange[] = [cellToRange(2, 2), cellToRange(3, 2), cellToRange(4, 2)];
const result = rangeMerge(testRange);
expect(result).toEqual([{ startColumn: 2, endColumn: 2, startRow: 2, endRow: 4 }] as IRange[]);
});
it('simple col', () => {
const testRange: IRange[] = [cellToRange(2, 2), cellToRange(2, 3), cellToRange(2, 4)];
const result = rangeMerge(testRange);
expect(result).toEqual([{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 2 }] as IRange[]);
});
it('simple col and simple row', () => {
const testRange: IRange[] = [
cellToRange(2, 2),
cellToRange(2, 3),
cellToRange(2, 4),
cellToRange(2, 2),
cellToRange(3, 2),
cellToRange(4, 2),
];
const result = rangeMerge(testRange);
expect(result).toEqual([
{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 2 },
{ startColumn: 2, endColumn: 2, startRow: 3, endRow: 4 },
] as IRange[]);
});
it('multiple range', () => {
const testRange1 = rangeMerge([cellToRange(2, 2), cellToRange(2, 3), cellToRange(2, 4)]);
const testRange2 = rangeMerge([cellToRange(3, 2), cellToRange(3, 3), cellToRange(3, 4)]);
const testRange3 = rangeMerge([cellToRange(4, 5), cellToRange(4, 6), cellToRange(4, 7)]);
//1 1 1
//2 2 2
//0 0 0 1 1 1
const result = rangeMerge([...testRange1, ...testRange3, ...testRange2]);
expect(result).toEqual([
{
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 4,
},
{
startRow: 4,
endRow: 4,
startColumn: 5,
endColumn: 7,
},
]);
//1 1 1 1
//2 2 2 1
//0 0 0 1 1 1
const result2 = rangeMerge([...testRange1, ...testRange3, ...testRange2, cellToRange(2, 5), cellToRange(3, 5)]);
expect(result2).toEqual([
{
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 5,
},
{
startRow: 4,
endRow: 4,
startColumn: 5,
endColumn: 7,
},
]);
//0 0 0 0 0 0
//0 0 0 0 0 0
//0 0 1 1 1 1
//0 0 2 0 2 2
//0 0 0 0 3 3 1 1
const result3 = rangeMerge([
cellToRange(2, 2),
cellToRange(2, 3),
cellToRange(2, 4),
cellToRange(2, 5),
cellToRange(3, 2),
cellToRange(3, 4),
cellToRange(3, 5),
cellToRange(4, 4),
cellToRange(4, 5),
cellToRange(4, 6),
cellToRange(4, 7),
]);
expect(result3).toEqual([
{
startRow: 2,
endRow: 4,
startColumn: 4,
endColumn: 5,
},
{
startRow: 2,
endRow: 2,
startColumn: 2,
endColumn: 3,
},
{
startRow: 4,
endRow: 4,
startColumn: 6,
endColumn: 7,
},
{
startRow: 3,
endRow: 3,
startColumn: 2,
endColumn: 2,
},
]);
});
it('hollow range', () => {
const result = rangeMerge([
{ startRow: 2, endRow: 3, startColumn: 2, endColumn: 10 },
{ startRow: 4, endRow: 6, startColumn: 2, endColumn: 4 },
{ startRow: 4, endRow: 6, startColumn: 8, endColumn: 10 },
{ startRow: 7, endRow: 10, startColumn: 2, endColumn: 10 },
]);
expect(result).toEqual([
{
startRow: 7,
endRow: 10,
startColumn: 2,
endColumn: 10,
},
{
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 10,
},
{
startRow: 4,
endRow: 6,
startColumn: 2,
endColumn: 4,
},
{
startRow: 4,
endRow: 6,
startColumn: 8,
endColumn: 10,
},
]);
});
it('test rangeMergeUtil', () => {
const testRange: IRange[] = [cellToRange(2, 2), cellToRange(3, 2), cellToRange(4, 2)];
expect(new RangeMergeUtil().add(...testRange).merge()).toEqual([
{ startColumn: 2, endColumn: 2, startRow: 2, endRow: 4 },
]);
expect(new RangeMergeUtil().add(...testRange).subtract(cellToRange(4, 2)).merge()).toEqual([
{ startColumn: 2, endColumn: 2, startRow: 2, endRow: 3 },
]);
expect(new RangeMergeUtil().add(...testRange).subtract(cellToRange(3, 2)).merge()).toEqual([
{ startColumn: 2, endColumn: 2, startRow: 2, endRow: 2 },
{ startColumn: 2, endColumn: 2, startRow: 4, endRow: 4 },
]);
expect(new RangeMergeUtil().add(...testRange).subtract(...testRange).merge()).toEqual([]);
});
it('should be no overlapping regions', () => {
const arr = [
{
startRow: 4,
endRow: 12,
startColumn: 1,
endColumn: 6,
},
{
startRow: 12,
endRow: 13,
startColumn: 2,
endColumn: 5,
},
{
startRow: 3,
endRow: 3,
startColumn: 2,
endColumn: 5,
},
];
const res = new RangeMergeUtil().add(...arr).merge();
expect(res).toEqual([
{ startRow: 4, endRow: 12, startColumn: 1, endColumn: 6 },
{ startRow: 3, endRow: 3, startColumn: 2, endColumn: 5 },
{ startRow: 13, endRow: 13, startColumn: 2, endColumn: 5 },
]);
});
});

View File

@@ -0,0 +1,104 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IColumnData, IRowData } from '@univerjs/core';
import { BooleanNumber } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { getOldColumnData, getOldRowData } from '../row-column-value';
describe('test getOldRowData', () => {
it('should return null if currentRow is null or undefined', () => {
expect(getOldRowData(null, {})).toBeNull();
expect(getOldRowData(undefined, {})).toBeUndefined();
});
it('should return a deep clone of currentRow if newRow is null or undefined', () => {
const currentRow: Partial<IRowData> = { h: 20, ia: BooleanNumber.TRUE };
const result = getOldRowData(currentRow, null);
expect(result).toEqual(currentRow);
expect(result).not.toBe(currentRow); // Ensure it's a deep clone
const result2 = getOldRowData(currentRow, undefined);
expect(result2).toEqual(currentRow);
expect(result2).not.toBe(currentRow); // Ensure it's a deep clone
});
it('should return only the changed properties from currentRow', () => {
const currentRow: Partial<IRowData> = { h: 20, ia: BooleanNumber.TRUE, ah: 30, hd: BooleanNumber.FALSE, s: 'style', custom: { key: 'customData' } };
const newRow: Partial<IRowData> = { h: 25, ia: BooleanNumber.FALSE };
const result = getOldRowData(currentRow, newRow);
expect(result).toEqual({ h: 20, ia: BooleanNumber.TRUE });
});
it('should return an empty object if no properties have changed', () => {
const currentRow: Partial<IRowData> = { h: 20, ia: BooleanNumber.TRUE };
const newRow: Partial<IRowData> = {};
const result = getOldRowData(currentRow, newRow);
expect(result).toEqual({});
});
it('should handle mixed property changes correctly', () => {
const currentRow: Partial<IRowData> = { h: 20, ia: BooleanNumber.TRUE, ah: 30, hd: BooleanNumber.FALSE, s: 'style', custom: { key: 'customData' } };
const newRow: Partial<IRowData> = { h: 25, ah: 40, custom: { key: 'newCustomData' } };
const result = getOldRowData(currentRow, newRow);
expect(result).toEqual({ h: 20, ah: 30, custom: { key: 'customData' } });
});
});
describe('test getOldColumnData', () => {
it('should return null if currenColumn is null or undefined', () => {
expect(getOldColumnData(null, {})).toBeNull();
expect(getOldColumnData(undefined, {})).toBeUndefined();
});
it('should return a deep clone of currenColumn if newColumn is null or undefined', () => {
const currenColumn: Partial<IColumnData> = { w: 20, hd: BooleanNumber.TRUE };
const result = getOldColumnData(currenColumn, null);
expect(result).toEqual(currenColumn);
expect(result).not.toBe(currenColumn); // Ensure it's a deep clone
const result2 = getOldColumnData(currenColumn, undefined);
expect(result2).toEqual(currenColumn);
expect(result2).not.toBe(currenColumn); // Ensure it's a deep clone
});
it('should return only the changed properties from currenColumn', () => {
const currenColumn: Partial<IColumnData> = { w: 20, hd: BooleanNumber.TRUE, s: 'style', custom: { key: 'customData' } };
const newColumn: Partial<IColumnData> = { w: 25, hd: BooleanNumber.FALSE };
const result = getOldColumnData(currenColumn, newColumn);
expect(result).toEqual({ w: 20, hd: BooleanNumber.TRUE });
});
it('should return an empty object if no properties have changed', () => {
const currenColumn: Partial<IColumnData> = { w: 20, hd: BooleanNumber.TRUE };
const newColumn: Partial<IColumnData> = {};
const result = getOldColumnData(currenColumn, newColumn);
expect(result).toEqual({});
});
it('should handle mixed property changes correctly', () => {
const currenColumn: Partial<IColumnData> = { w: 20, hd: BooleanNumber.FALSE, s: 'style', custom: { key: 'customData' } };
const newColumn: Partial<IColumnData> = { w: 25, custom: { key: 'newCustomData' } };
const result = getOldColumnData(currenColumn, newColumn);
expect(result).toEqual({ w: 20, custom: { key: 'customData' } });
});
});

View File

@@ -0,0 +1,263 @@
/**
* 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, IRange } from '@univerjs/core';
import { cellToRange, IUniverInstanceService } from '@univerjs/core';
import { describe, expect, it, vi } from 'vitest';
import {
createUniqueKey,
discreteRangeToRange,
findFirstNonEmptyCell,
generateNullCell,
generateNullCellStyle,
generateNullCellValue,
getActiveWorksheet,
getVisibleRanges,
groupByKey,
rangeToDiscreteRange,
} from '../utils';
function createAccessor(getter: (token: unknown) => unknown): IAccessor {
return {
get: (token: unknown) => getter(token) as never,
has: () => true,
} as unknown as IAccessor;
}
describe('Test utils', () => {
it('Test generateNullCell', () => {
const range: IRange[] = [cellToRange(0, 0), cellToRange(1, 1)];
const result = generateNullCell(range);
expect(result).toEqual({
0: {
0: null,
},
1: {
1: null,
},
});
});
it('Test generateNullCellValue', () => {
const range: IRange[] = [cellToRange(0, 0), cellToRange(1, 1)];
const result = generateNullCellValue(range);
expect(result).toEqual({
0: {
0: {
v: null,
p: null,
f: null,
si: null,
custom: null,
},
},
1: {
1: { v: null, p: null, f: null, si: null, custom: null },
},
});
});
it('Test generateNullCellStyle', () => {
const range: IRange[] = [cellToRange(0, 0), cellToRange(1, 1)];
const result = generateNullCellStyle(range);
expect(result).toEqual({
0: {
0: {
s: null,
},
},
1: {
1: {
s: null,
},
},
});
});
it('Test groupByKey', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
const grouped = groupByKey(
[
{ key: 'fallback', v: 1 },
{ key: 'a', v: 2 },
{ key: '', v: 3 },
{ key: 42, v: 4 } as unknown as { key: string; v: number },
],
'key',
'fallback'
);
expect(grouped.a).toEqual([{ key: 'a', v: 2 }]);
expect(grouped.fallback).toEqual([
{ key: 'fallback', v: 1 },
{ key: '', v: 3 },
]);
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
it('Test createUniqueKey', () => {
const getKey = createUniqueKey(2);
expect(getKey()).toBe(2);
expect(getKey()).toBe(3);
});
it('Test findFirstNonEmptyCell', () => {
const worksheet = {
getCell: (row: number, col: number) => {
if (row === 1 && col === 0) {
return { p: { body: {} } };
}
return null;
},
};
const range = { startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 };
expect(findFirstNonEmptyCell(range, worksheet as never)).toEqual({
startRow: 1,
endRow: 1,
startColumn: 0,
endColumn: 0,
});
});
it('Test getActiveWorksheet', () => {
const worksheet = { id: 'sheet-1' };
const workbook = {
getActiveSheet: () => worksheet,
};
const instanceService = {
getCurrentUnitOfType: () => workbook,
};
const [currentWorkbook, currentWorksheet] = getActiveWorksheet(instanceService as never);
expect(currentWorkbook).toBe(workbook);
expect(currentWorksheet).toBe(worksheet);
});
it('Test discreteRangeToRange', () => {
expect(
discreteRangeToRange({
rows: [2, 4, 5],
cols: [3, 6],
})
).toEqual({
startRow: 2,
endRow: 5,
startColumn: 3,
endColumn: 6,
});
});
it('Test rangeToDiscreteRange', () => {
const worksheet = {
getRowFiltered: (row: number) => row === 1,
};
const workbook = {
getSheetBySheetId: () => worksheet,
getActiveSheet: () => worksheet,
};
const instanceService = {
getUnit: () => workbook,
getCurrentUnitOfType: () => workbook,
};
const accessor = createAccessor((token) => {
if (token === IUniverInstanceService) {
return instanceService;
}
return null;
});
expect(
rangeToDiscreteRange(
{ startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 },
accessor,
'unit-1',
'sheet-1'
)
).toEqual({
rows: [0, 2],
cols: [0, 1],
});
});
it('Test rangeToDiscreteRange returns null without worksheet', () => {
const instanceService = {
getUnit: () => null,
getCurrentUnitOfType: () => null,
};
const accessor = createAccessor((token) => {
if (token === IUniverInstanceService) {
return instanceService;
}
return null;
});
expect(
rangeToDiscreteRange(
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
accessor
)
).toBeNull();
});
it('Test getVisibleRanges', () => {
const worksheet = {
getRowFiltered: (row: number) => row === 1 || row === 4,
getSheetId: () => 'sheet-1',
};
const workbook = {
getSheetBySheetId: () => worksheet,
getActiveSheet: () => worksheet,
getUnitId: () => 'unit-1',
};
const instanceService = {
getCurrentUnitOfType: () => workbook,
getUnit: () => workbook,
};
const accessor = createAccessor((token) => {
if (token === IUniverInstanceService) {
return instanceService;
}
return null;
});
const ranges = [
{ startRow: 0, endRow: 3, startColumn: 0, endColumn: 0 },
{ startRow: 4, endRow: 6, startColumn: 0, endColumn: 0 },
{ startRow: 0, endRow: 1, startColumn: 1, endColumn: 1 },
];
expect(getVisibleRanges(ranges, accessor, 'unit-1', 'sheet-1')).toEqual([
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
{ startRow: 2, endRow: 3, startColumn: 0, endColumn: 0 },
{ startRow: 5, endRow: 6, startColumn: 0, endColumn: 0 },
{ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 },
]);
});
it('Test getVisibleRanges fallback without target', () => {
const accessor = createAccessor((token) => {
if (token === IUniverInstanceService) {
return {
getCurrentUnitOfType: () => null,
};
}
return null;
});
const ranges = [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 }];
expect(getVisibleRanges(ranges, accessor)).toEqual(ranges);
});
});

View File

@@ -0,0 +1,27 @@
/**
* 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.
*/
export interface ICellOverGridPosition {
column: number; // column number
columnOffset: number; // column offset, pixel unit
row: number; // row number
rowOffset: number; // row offset, pixel unit
}
export interface ISheetOverGridPosition {
from: ICellOverGridPosition; // Start position in its own sheet.
to: ICellOverGridPosition; // End position in its own sheet.
}

View File

@@ -0,0 +1,520 @@
/**
* 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 {
BorderKey,
BorderStyleKey,
ColorStyleKey,
IBorderData,
IBorderStyleData,
ICellData,
IColorStyle,
IDocumentData,
IPaddingData,
IParagraph,
IStyleData,
ITextDecoration,
ITextRotation,
ITextRun,
ITextStyle,
Nullable,
PaddingKey,
StyleKey,
Styles,
TextDecorationKey,
TextRotationKey,
} from '@univerjs/core';
import {
BORDER_KEYS,
BORDER_STYLE_KEYS,
COLOR_STYLE_KEYS,
normalizeTextRuns,
PADDING_KEYS,
STYLE_KEYS,
TEXT_DECORATION_KEYS,
TEXT_ROTATION_KEYS,
Tools,
} from '@univerjs/core';
type UnknownRecord = Record<string, unknown>;
type BorderRecord = Partial<Record<BorderKey, Nullable<IBorderStyleData>>>;
function isPlainRecord(value: unknown): value is UnknownRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function forEachPresentKey<K extends string>(
value: UnknownRecord,
keys: readonly K[],
iteratee: (key: K, fieldValue: unknown) => void
) {
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
iteratee(key, value[key]);
}
}
function sanitizeFixedShapeObject<T extends object, K extends string>(
value: unknown,
directKeys: readonly K[],
nestedSanitizers?: Record<string, (value: unknown) => unknown>
): Nullable<T> | undefined {
if (value == null) {
return value as null | undefined;
}
if (!isPlainRecord(value)) {
return undefined;
}
const sanitized: UnknownRecord = {};
let hasFields = false;
forEachPresentKey(value, directKeys, (key, fieldValue) => {
sanitized[key] = fieldValue;
hasFields = true;
});
if (nestedSanitizers) {
for (const key in nestedSanitizers) {
const sanitize = nestedSanitizers[key];
if (!sanitize || !Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const sanitizedValue = sanitize(value[key]);
if (sanitizedValue !== undefined) {
sanitized[key] = sanitizedValue;
hasFields = true;
}
}
}
return hasFields ? (sanitized as T) : undefined;
}
function sanitizeMappedObject<T extends object, K extends string>(
value: unknown,
keys: readonly K[],
sanitize: (value: unknown) => unknown
): Nullable<T> | undefined {
if (value == null) {
return value as null | undefined;
}
if (!isPlainRecord(value)) {
return undefined;
}
const sanitized: UnknownRecord = {};
let hasFields = false;
forEachPresentKey(value, keys, (key, fieldValue) => {
const sanitizedValue = sanitize(fieldValue);
if (sanitizedValue !== undefined) {
sanitized[key] = sanitizedValue;
hasFields = true;
}
});
return hasFields ? (sanitized as T) : undefined;
}
function shouldSkipRichTextStyleKey(key: StyleKey): boolean {
switch (key) {
case 'bd':
case 'tr':
case 'td':
case 'ht':
case 'vt':
case 'tb':
case 'pd':
case 'bg':
return true;
default:
return false;
}
}
function isSerializablePrimitive(value: unknown): value is string | number | boolean | null | undefined {
return value == null || ['string', 'number', 'boolean'].includes(typeof value);
}
function sanitizeColorStyle(value: unknown): Nullable<IColorStyle> | undefined {
return sanitizeFixedShapeObject<IColorStyle, ColorStyleKey>(value, COLOR_STYLE_KEYS);
}
function sanitizeTextDecoration(value: unknown): Nullable<ITextDecoration> | undefined {
return sanitizeFixedShapeObject<ITextDecoration, TextDecorationKey>(value, TEXT_DECORATION_KEYS, {
cl: sanitizeColorStyle,
});
}
function sanitizeBorderStyleData(value: unknown): Nullable<IBorderStyleData> | undefined {
return sanitizeFixedShapeObject<IBorderStyleData, BorderStyleKey>(value, BORDER_STYLE_KEYS, {
cl: sanitizeColorStyle,
});
}
function sanitizeBorderData(value: unknown): Nullable<IBorderData> | undefined {
return sanitizeMappedObject<IBorderData, BorderKey>(value, BORDER_KEYS, sanitizeBorderStyleData);
}
function sanitizeTextRotation(value: unknown): Nullable<ITextRotation> | undefined {
return sanitizeFixedShapeObject<ITextRotation, TextRotationKey>(value, TEXT_ROTATION_KEYS);
}
function sanitizePaddingData(value: unknown): Nullable<IPaddingData> | undefined {
return sanitizeFixedShapeObject<IPaddingData, PaddingKey>(value, PADDING_KEYS);
}
function sanitizeNumberFormat(value: unknown): Nullable<{ pattern: string }> | undefined {
if (value == null) {
return value as null | undefined;
}
if (!isPlainRecord(value) || !Object.prototype.hasOwnProperty.call(value, 'pattern') || typeof value.pattern !== 'string') {
return undefined;
}
return { pattern: value.pattern };
}
function sanitizeStyleValue(key: StyleKey, value: unknown): unknown {
switch (key) {
case 'ul':
case 'bbl':
case 'st':
case 'ol':
return sanitizeTextDecoration(value);
case 'bg':
case 'cl':
return sanitizeColorStyle(value);
case 'bd':
return sanitizeBorderData(value);
case 'tr':
return sanitizeTextRotation(value);
case 'pd':
return sanitizePaddingData(value);
case 'n':
return sanitizeNumberFormat(value);
default:
return isSerializablePrimitive(value) ? value : undefined;
}
}
function mergeBorderData(
currentBorders: Nullable<IBorderData>,
incomingBorders: Nullable<IBorderData>
): Nullable<IBorderData> {
if (incomingBorders === null) {
return null;
}
if (incomingBorders === undefined) {
return currentBorders;
}
const borderRecord = incomingBorders as BorderRecord;
const mergedBorders: BorderRecord = Tools.isObject(currentBorders) ? { ...(currentBorders as BorderRecord) } : {};
forEachPresentKey(borderRecord as UnknownRecord, BORDER_KEYS, (key, fieldValue) => {
const borderStyle = sanitizeBorderStyleData(fieldValue);
if (borderStyle !== undefined) {
mergedBorders[key] = borderStyle;
}
});
return mergedBorders as IBorderData;
}
/**
*
* @param styles
* @param oldVal
* @param newVal
*/
export function handleStyle(styles: Styles, oldVal: ICellData, newVal: ICellData) {
// use null to clear style
const oldStyle = styles.getStyleByCell(oldVal);
if (oldStyle == null) {
// clear
delete oldVal.s;
}
if (typeof newVal.s === 'string') {
newVal.s = styles.get(newVal.s);
}
// set style
const merge = mergeStyle(oldStyle, newVal.s ? (newVal.s as Nullable<IStyleData>) : null);
// then remove null
if (merge) {
Tools.removeNull(merge);
// remove empty object
Object.entries(merge).forEach(([key, val]) => {
if (typeof val === 'object' && val !== null && Object.keys(val).length === 0) {
delete merge[key as keyof IStyleData];
}
});
}
if (Tools.isEmptyObject(merge)) {
delete oldVal.s;
} else {
oldVal.s = styles.setValue(merge);
}
const newValueStream = newVal.v ? `${newVal.v}\r\n` : '';
// Only need to copy newValue.s to oldValue.p when you modify the cell style, not when you modify the cell value.
if (!newVal.p && oldVal.p) {
if (newValueStream && newValueStream !== oldVal.p.body?.dataStream) {
delete oldVal.p;
} else {
mergeRichTextStyle(oldVal.p, newVal.s ? (newVal.s as Nullable<IStyleData>) : null);
}
}
}
/**
* Convert old style data for storage
* @param style
* @param oldStyle
* @param newStyle
*/
export function transformStyle(oldStyle: Nullable<IStyleData>, newStyle: Nullable<IStyleData>): Nullable<IStyleData> {
// If there is no newly set style, directly store the historical style
if (!newStyle || !Object.keys(newStyle).length) {
return oldStyle;
}
const backupStyle = (Tools.deepClone(oldStyle ?? {}) || {}) as UnknownRecord;
const styleRecord = newStyle as UnknownRecord;
for (const key of STYLE_KEYS) {
if (!Object.prototype.hasOwnProperty.call(styleRecord, key)) {
continue;
}
const sanitizedValue = sanitizeStyleValue(key, styleRecord[key]);
if (sanitizedValue === undefined) {
continue;
}
if (key === 'bd') {
backupStyle[key] = transformBorders((backupStyle[key] as IBorderData) || {}, sanitizedValue as Nullable<IBorderData>);
}
// 1. To modify the existing style,we need original setting to undo
// 2. Newly set the style, we need null to undo
else if (!(key in backupStyle)) {
backupStyle[key] = null;
}
}
return backupStyle as Nullable<IStyleData>;
}
/**
* Convert old style border for storage
* @param style
* @param oldBorders
* @param newBorders
*/
function transformBorders(oldBorders: IBorderData, newBorders: Nullable<IBorderData>): IBorderData {
// If there is no newly set border, directly store the historical border
if (!newBorders || !Object.keys(newBorders).length) {
return oldBorders;
}
const oldBorderRecord = oldBorders as BorderRecord;
for (const key of BORDER_KEYS) {
if (!Object.prototype.hasOwnProperty.call(newBorders as UnknownRecord, key)) {
continue;
}
// 1. To modify the existing border,we need original setting to undo
// 2. Newly set the border, we need null to undo
if (!(key in oldBorders)) {
oldBorderRecord[key] = null;
}
}
return oldBorders;
}
/**
* merge new style to old style
* @param oldStyle
* @param newStyle
* @param isRichText
*/
function mergeStyle(
oldStyle: Nullable<IStyleData>,
newStyle: Nullable<IStyleData>,
isRichText: boolean = false
): Nullable<IStyleData> {
// clear style
if (newStyle === null) return newStyle;
// don't operate
if (newStyle === undefined) return oldStyle;
const backupStyle = (Tools.deepClone(oldStyle) || {}) as UnknownRecord;
const styleRecord = newStyle as UnknownRecord;
for (const key of STYLE_KEYS) {
if (!Object.prototype.hasOwnProperty.call(styleRecord, key)) {
continue;
}
// Do not copy cell background color to rich text background color.
if (isRichText && shouldSkipRichTextStyleKey(key)) {
continue;
}
const sanitizedValue = sanitizeStyleValue(key, styleRecord[key]);
if (sanitizedValue === undefined) {
continue;
}
// you can only choose one of the themeColor and rgbColor of the border setting
if (key === 'bd') {
backupStyle[key] = mergeBorderData(backupStyle[key] as Nullable<IBorderData>, sanitizedValue as Nullable<IBorderData>);
} else {
backupStyle[key] = sanitizedValue;
}
}
if ('cl' in backupStyle) {
const backupStyleData = backupStyle as IStyleData;
const color = backupStyleData.cl;
if ('ul' in backupStyleData && backupStyleData.ul) {
backupStyleData.ul.cl = color as IColorStyle;
}
if ('ol' in backupStyleData && backupStyleData.ol) {
backupStyleData.ol.cl = color as IColorStyle;
}
if ('st' in backupStyleData && backupStyleData.st) {
backupStyleData.st.cl = color as IColorStyle;
}
}
return backupStyle as Nullable<IStyleData>;
}
/**
*
* @param paragraphs
* @param offset
*/
function skipParagraphs(paragraphs: IParagraph[], offset: number): number {
if (paragraphs.some((p) => p.startIndex === offset)) {
return skipParagraphs(paragraphs, offset + 1);
}
return offset;
}
/**
* Find the text style of all paragraphs and modify it to the new style
* @param p
* @param newStyle
*/
function mergeRichTextStyle(p: IDocumentData, newStyle: Nullable<IStyleData>) {
if (p.body == null) {
return;
}
if (!Array.isArray(p.body.textRuns)) {
p.body.textRuns = [];
}
let index = 0;
const newTextRuns = [];
const paragraphs = p.body?.paragraphs || [];
for (const textRun of p.body.textRuns) {
const { st, ed, ts = {} } = textRun;
if (index < st) {
const tr: ITextRun = {
st: index,
ed: st,
};
const merge = mergeStyle({}, newStyle, true);
// then remove null
merge && Tools.removeNull(merge);
if (!Tools.isEmptyObject(merge)) {
tr.ts = merge!;
}
newTextRuns.push(tr);
}
const merge = mergeStyle(ts, newStyle, true);
// then remove null
merge && Tools.removeNull(merge);
if (Tools.isEmptyObject(merge)) {
delete textRun.ts;
} else {
textRun.ts = merge as ITextStyle;
}
newTextRuns.push(textRun);
index = skipParagraphs(paragraphs, ed);
}
const endIndex = p.body.dataStream.endsWith('\r\n') ? p.body.dataStream.length - 2 : p.body.dataStream.length;
if (index < endIndex) {
const tr: ITextRun = {
st: index,
ed: endIndex,
};
const merge = mergeStyle({}, newStyle, true);
// then remove null
merge && Tools.removeNull(merge);
if (!Tools.isEmptyObject(merge)) {
tr.ts = merge!;
}
newTextRuns.push(tr);
}
p.body.textRuns = normalizeTextRuns(newTextRuns);
}

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 { CellValue, ICellData, IDocumentData, IStyleData, Nullable, Styles } from '@univerjs/core';
import { CellValueType, isBooleanString, isRealNum, isTextFormat, willLoseNumericPrecision } from '@univerjs/core';
export function isCellImage(documentData: Nullable<IDocumentData>): boolean {
return !!documentData?.drawingsOrder?.length;
}
/**
* Get cell value type by style, new value and old value.
* If the new value contains t, then take t directly. In other cases, we need to dynamically determine based on actual data and styles
* @param newVal
* @param oldVal
* @returns
*/
export function getCellType(styles: Styles, newVal: ICellData, oldVal: ICellData) {
if (newVal.t) return newVal.t;
if (newVal.v === null) return null;
const newStyle = styles.getStyleByCell(newVal);
const oldStyle = styles.getStyleByCell(oldVal);
if (oldVal.t === CellValueType.FORCE_STRING) {
// For cells with forced strings, set the percentage format and still use forced strings
// For cells with forced strings, update to the number and convert to ordinary numbers
// For cells with forced strings and text formats, update to the number and still use forced strings
if (!isTextFormat(oldStyle?.n?.pattern) && newVal.v !== undefined) {
if (isRealNum(newVal.v)) {
return CellValueType.NUMBER;
} else if (isBooleanString(`${newVal.v}`)) {
return CellValueType.BOOLEAN;
}
}
return CellValueType.FORCE_STRING;
}
// For cells with ordinary numbers, set the text format and convert the cell type to text
if (hasNumberFormat(newStyle)) {
if (isTextFormat(newStyle?.n?.pattern)) {
return CellValueType.STRING;
}
return checkCellValueTypeByValue(newVal, oldVal);
}
// For cells with text format, the cell type is still text when numbers are written, because the ISNUMBER function detects FALSE
if (isTextFormat(oldStyle?.n?.pattern)) {
return CellValueType.STRING;
}
return checkCellValueTypeByValue(newVal, oldVal);
}
function checkCellValueTypeByValue(newVal: ICellData, oldVal: ICellData): Nullable<CellValueType> {
return newVal.v !== undefined ? checkCellValueType(newVal.v, newVal.t) : checkCellValueType(oldVal.v, oldVal.t);
}
function hasNumberFormat(style: Nullable<IStyleData>) {
return !!style?.n?.pattern;
}
/**
* Get the correct type after setting values to a cell.
*
* @param v the new value
* @param type the old type
* @returns the new type
*/
export function checkCellValueType(v: Nullable<CellValue>, type: Nullable<CellValueType>): Nullable<CellValueType> {
if (v === null) return null;
if (typeof v === 'string') {
// Support scientific notation, such as 10e+1
if (isRealNum(v)) {
if ((+v === 0 || +v === 1) && type === CellValueType.BOOLEAN) {
return CellValueType.BOOLEAN;
}
// If the cell type is string or force string, do not need judge whether it will lose precision.
// If the numeric string will lose precision when converted to a number, set the cell type to force string
// e.g. 123456789123456789
// e.g. 1212121212121212.2345
if (type !== CellValueType.STRING && type !== CellValueType.FORCE_STRING && willLoseNumericPrecision(v)) {
return CellValueType.FORCE_STRING;
}
return CellValueType.NUMBER;
}
if (isBooleanString(v)) {
return CellValueType.BOOLEAN;
}
return CellValueType.STRING;
}
if (typeof v === 'number') {
if ((v === 0 || v === 1) && type === CellValueType.BOOLEAN) {
return CellValueType.BOOLEAN;
}
return CellValueType.NUMBER;
}
if (typeof v === 'boolean') {
return CellValueType.BOOLEAN;
}
return CellValueType.FORCE_STRING;
}
export function getCellTypeByPattern(cell: ICellData, pattern: string) {
if (cell.t === CellValueType.FORCE_STRING) {
return CellValueType.FORCE_STRING;
}
if (isTextFormat(pattern)) {
return CellValueType.STRING;
}
return checkCellValueType(cell.v, cell.t);
}

View File

@@ -0,0 +1,126 @@
/**
* 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, Nullable } from '@univerjs/core';
import { CellValueType, isSafeNumeric } from '@univerjs/core';
/**
* Get cell value from new value by type
* @param type
* @param cell
* @returns
*/
export function getCellValue(type: Nullable<CellValueType>, cell: ICellData) {
if (cell.v === undefined || cell.v === null) {
return cell.v;
}
if (type === CellValueType.NUMBER) {
return Number(cell.v);
}
if (type === CellValueType.BOOLEAN) {
return extractBooleanValue(cell.v) ? 1 : 0;
}
if (type === CellValueType.STRING || type === CellValueType.FORCE_STRING) {
return `${cell.v}`;
}
return cell.v;
}
/**
* Check if the value can be casted to a boolean.
* @internal
* @param value
* @returns It would return null if the value cannot be casted to a boolean, and would return the boolean value if it can be casted.
*/
export function extractBooleanValue(value: Nullable<string | number | boolean>): Nullable<boolean> {
if (typeof value === 'string') {
if (value.toUpperCase() === 'TRUE') {
return true;
};
if (value.toUpperCase() === 'FALSE') {
return false;
}
if (isSafeNumeric(value)) {
if (Number(value) === 0) {
return false;
}
if (Number(value) === 1) {
return true;
}
}
}
if (typeof value === 'number') {
if (value === 0) {
return false;
}
if (value === 1) {
return true;
}
}
if (typeof value === 'boolean') {
return value;
}
return null;
}
/**
* Supplement the data of the cell, set the other value to NULL, Used to reset properties when undoing
* @param value
* @returns
*/
export function setNull(value: Nullable<ICellData>) {
if (value == null) return null;
if (value.f === undefined) {
value.f = null;
}
if (value.si === undefined) {
value.si = null;
}
if (value.p === undefined) {
value.p = null;
}
if (value.v === undefined) {
value.v = null;
}
if (value.t === undefined) {
value.t = null;
}
if (value.s === undefined) {
value.s = null;
}
if (value.custom === undefined) {
value.custom = null;
}
return value;
}

View File

@@ -0,0 +1,530 @@
/**
* 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 { ICommandInfo, IRange, IUniverInstanceService } from '@univerjs/core';
import type { IMoveRangeMutationParams } from '../../commands/mutations/move-range.mutation';
import type { IMoveColumnsMutationParams, IMoveRowsMutationParams } from '../../commands/mutations/move-rows-cols.mutation';
import type { IReorderRangeMutationParams } from '../../commands/mutations/reorder-range.mutation';
import type { ISetColDataMutationParams } from '../../commands/mutations/set-col-data.mutation';
import type { ISetColHiddenMutationParams, ISetColVisibleMutationParams } from '../../commands/mutations/set-col-visible.mutation';
import type { ISetGridlinesColorMutationParams } from '../../commands/mutations/set-gridlines-color.mutation';
import type { ISetRangeValuesMutationParams } from '../../commands/mutations/set-range-values.mutation';
import type { ISetRowDataMutationParams } from '../../commands/mutations/set-row-data.mutation';
import type { ISetRowHiddenMutationParams, ISetRowVisibleMutationParams } from '../../commands/mutations/set-row-visible.mutation';
import type { ISetWorksheetColWidthMutationParams } from '../../commands/mutations/set-worksheet-col-width.mutation';
import type { ISetWorksheetDefaultStyleMutationParams } from '../../commands/mutations/set-worksheet-default-style.mutation';
import type {
ISetWorksheetRowAutoHeightMutationParams,
ISetWorksheetRowHeightMutationParams,
ISetWorksheetRowIsAutoHeightMutationParams,
} from '../../commands/mutations/set-worksheet-row-height.mutation';
import type { IToggleGridlinesMutationParams } from '../../commands/mutations/toggle-gridlines.mutation';
import type { ISetWorksheetActiveOperationParams } from '../../commands/operations/set-worksheet-active.operation';
import type { IAddWorksheetMergeMutationParams, IInsertColMutationParams, IInsertRowMutationParams, IRemoveColMutationParams, IRemoveRowsMutationParams, IRemoveWorksheetMergeMutationParams, IWorksheetRangeThemeStyleMutationParams } from '../interfaces';
import { ObjectMatrix, RANGE_TYPE } from '@univerjs/core';
import { getSheetCommandTarget } from '../../commands/commands/utils/target-util';
import { AddWorksheetMergeMutation } from '../../commands/mutations/add-worksheet-merge.mutation';
import { SetWorksheetRangeThemeStyleMutation } from '../../commands/mutations/add-worksheet-range-theme.mutation';
import { DeleteWorksheetRangeThemeStyleMutation } from '../../commands/mutations/delete-worksheet-range-theme.mutation';
import { InsertColMutation, InsertRowMutation } from '../../commands/mutations/insert-row-col.mutation';
import { MoveRangeMutation } from '../../commands/mutations/move-range.mutation';
import { MoveColsMutation, MoveRowsMutation } from '../../commands/mutations/move-rows-cols.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../commands/mutations/remove-row-col.mutation';
import { RemoveWorksheetMergeMutation } from '../../commands/mutations/remove-worksheet-merge.mutation';
import { ReorderRangeMutation } from '../../commands/mutations/reorder-range.mutation';
import { SetColDataMutation } from '../../commands/mutations/set-col-data.mutation';
import { SetColHiddenMutation, SetColVisibleMutation } from '../../commands/mutations/set-col-visible.mutation';
import { SetGridlinesColorMutation } from '../../commands/mutations/set-gridlines-color.mutation';
import { SetRangeValuesMutation } from '../../commands/mutations/set-range-values.mutation';
import { SetRowDataMutation } from '../../commands/mutations/set-row-data.mutation';
import { SetRowHiddenMutation, SetRowVisibleMutation } from '../../commands/mutations/set-row-visible.mutation';
import { SetWorksheetColWidthMutation } from '../../commands/mutations/set-worksheet-col-width.mutation';
import { SetWorksheetColumnCountMutation } from '../../commands/mutations/set-worksheet-column-count.mutation';
import { SetWorksheetDefaultStyleMutation } from '../../commands/mutations/set-worksheet-default-style.mutation';
import { SetWorksheetRowCountMutation } from '../../commands/mutations/set-worksheet-row-count.mutation';
import {
SetWorksheetRowAutoHeightMutation,
SetWorksheetRowHeightMutation,
SetWorksheetRowIsAutoHeightMutation,
} from '../../commands/mutations/set-worksheet-row-height.mutation';
import { ToggleGridlinesMutation } from '../../commands/mutations/toggle-gridlines.mutation';
import { SetWorksheetActiveOperation } from '../../commands/operations/set-worksheet-active.operation';
/**
* Enum for all skeleton change command IDs
*/
export enum SheetSkeletonChangeType {
SET_WORKSHEET_ROW_HEIGHT = 'sheet.mutation.set-worksheet-row-height',
SET_WORKSHEET_ROW_IS_AUTO_HEIGHT = 'sheet.mutation.set-worksheet-row-is-auto-height',
SET_WORKSHEET_ROW_AUTO_HEIGHT = 'sheet.mutation.set-worksheet-row-auto-height',
SET_WORKSHEET_COL_WIDTH = 'sheet.mutation.set-worksheet-col-width',
SET_WORKSHEET_ACTIVE = 'sheet.operation.set-worksheet-active',
MOVE_ROWS = 'sheet.mutation.move-rows',
MOVE_COLUMNS = 'sheet.mutation.move-columns',
SET_COL_HIDDEN = 'sheet.mutation.set-col-hidden',
SET_COL_VISIBLE = 'sheet.mutation.set-col-visible',
SET_ROW_HIDDEN = 'sheet.mutation.set-row-hidden',
SET_ROW_VISIBLE = 'sheet.mutation.set-row-visible',
INSERT_COL = 'sheet.mutation.insert-col',
INSERT_ROW = 'sheet.mutation.insert-row',
REMOVE_COL = 'sheet.mutation.remove-col',
REMOVE_ROW = 'sheet.mutation.remove-rows',
TOGGLE_GRIDLINES = 'sheet.mutation.toggle-gridlines',
SET_GRIDLINES_COLOR = 'sheet.mutation.set-gridlines-color',
}
/**
* Enum for all value change command IDs
*/
export enum SheetValueChangeType {
SET_RANGE_VALUES = 'sheet.mutation.set-range-values',
MOVE_RANGE = 'sheet.mutation.move-range',
REMOVE_WORKSHEET_MERGE = 'sheet.mutation.remove-worksheet-merge',
ADD_WORKSHEET_MERGE = 'sheet.mutation.add-worksheet-merge',
REORDER_RANGE = 'sheet.mutation.reorder-range',
SET_WORKSHEET_DEFAULT_STYLE = 'sheet.mutation.set-worksheet-default-style',
SET_ROW_DATA = 'sheet.mutation.set-row-data',
SET_COL_DATA = 'sheet.mutation.set-col-data',
SET_WORKSHEET_RANGE_THEME_STYLE = 'sheet.mutation.set-worksheet-range-theme-style',
DELETE_WORKSHEET_RANGE_THEME_STYLE = 'sheet.mutation.delete-worksheet-range-theme-style',
}
/**
* Mutations those will trigger the skeleton change.
*/
export const COMMAND_LISTENER_SKELETON_CHANGE = [
SetWorksheetRowHeightMutation.id,
SetWorksheetRowIsAutoHeightMutation.id,
SetWorksheetRowAutoHeightMutation.id,
SetWorksheetColWidthMutation.id,
SetWorksheetActiveOperation.id,
MoveRowsMutation.id,
MoveColsMutation.id,
SetColHiddenMutation.id,
SetColVisibleMutation.id,
SetRowHiddenMutation.id,
SetRowVisibleMutation.id,
InsertColMutation.id,
InsertRowMutation.id,
RemoveColMutation.id,
RemoveRowMutation.id,
ToggleGridlinesMutation.id,
SetGridlinesColorMutation.id,
SetWorksheetRowCountMutation.id,
SetWorksheetColumnCountMutation.id,
];
export const COMMAND_LISTENER_VALUE_CHANGE = [
SetRangeValuesMutation.id,
MoveRangeMutation.id,
RemoveWorksheetMergeMutation.id,
AddWorksheetMergeMutation.id,
ReorderRangeMutation.id,
SetWorksheetDefaultStyleMutation.id,
SetRowDataMutation.id,
SetColDataMutation.id,
SetWorksheetRangeThemeStyleMutation.id,
DeleteWorksheetRangeThemeStyleMutation.id,
];
export type CommandListenerSkeletonChange =
| {
id: SheetSkeletonChangeType.SET_WORKSHEET_ROW_HEIGHT;
params: ISetWorksheetRowHeightMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_WORKSHEET_ROW_IS_AUTO_HEIGHT;
params: ISetWorksheetRowIsAutoHeightMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_WORKSHEET_ROW_AUTO_HEIGHT;
params: ISetWorksheetRowAutoHeightMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_WORKSHEET_COL_WIDTH;
params: ISetWorksheetColWidthMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_WORKSHEET_ACTIVE;
params: ISetWorksheetActiveOperationParams;
}
| {
id: SheetSkeletonChangeType.MOVE_ROWS;
params: IMoveRowsMutationParams;
}
| {
id: SheetSkeletonChangeType.MOVE_COLUMNS;
params: IMoveColumnsMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_COL_HIDDEN;
params: ISetColHiddenMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_COL_VISIBLE;
params: ISetColVisibleMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_ROW_HIDDEN;
params: ISetRowHiddenMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_ROW_VISIBLE;
params: ISetRowVisibleMutationParams;
}
| {
id: SheetSkeletonChangeType.INSERT_COL;
params: IInsertColMutationParams;
}
| {
id: SheetSkeletonChangeType.INSERT_ROW;
params: IInsertRowMutationParams;
}
| {
id: SheetSkeletonChangeType.REMOVE_COL;
params: IRemoveColMutationParams;
}
| {
id: SheetSkeletonChangeType.REMOVE_ROW;
params: IRemoveRowsMutationParams;
}
| {
id: SheetSkeletonChangeType.TOGGLE_GRIDLINES;
params: IToggleGridlinesMutationParams;
}
| {
id: SheetSkeletonChangeType.SET_GRIDLINES_COLOR;
params: ISetGridlinesColorMutationParams;
};
export type CommandListenerValueChange =
| {
id: SheetValueChangeType.SET_RANGE_VALUES;
params: ISetRangeValuesMutationParams;
}
| {
id: SheetValueChangeType.MOVE_RANGE;
params: IMoveRangeMutationParams;
}
| {
id: SheetValueChangeType.REMOVE_WORKSHEET_MERGE;
params: IRemoveWorksheetMergeMutationParams;
}
| {
id: SheetValueChangeType.ADD_WORKSHEET_MERGE;
params: IAddWorksheetMergeMutationParams;
}
| {
id: SheetValueChangeType.REORDER_RANGE;
params: IReorderRangeMutationParams;
}
| {
id: SheetValueChangeType.SET_WORKSHEET_DEFAULT_STYLE;
params: ISetWorksheetDefaultStyleMutationParams;
}
| {
id: SheetValueChangeType.SET_ROW_DATA;
params: ISetRowDataMutationParams;
}
| {
id: SheetValueChangeType.SET_COL_DATA;
params: ISetColDataMutationParams;
}
| {
id: SheetValueChangeType.SET_WORKSHEET_RANGE_THEME_STYLE;
params: IWorksheetRangeThemeStyleMutationParams;
}
| {
id: SheetValueChangeType.DELETE_WORKSHEET_RANGE_THEME_STYLE;
params: IWorksheetRangeThemeStyleMutationParams;
};
// eslint-disable-next-line max-lines-per-function
export function getValueChangedEffectedRange(univerInstanceService: IUniverInstanceService, commandInfo: ICommandInfo): { unitId: string; subUnitId: string; range: IRange }[] {
switch (commandInfo.id) {
case SheetValueChangeType.SET_RANGE_VALUES: {
const params = commandInfo.params as ISetRangeValuesMutationParams;
const range = new ObjectMatrix(params.cellValue).getDataRange();
if (range.endRow === -1) return [];
return params.cellValue
? [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range,
}]
: [];
}
case SheetValueChangeType.MOVE_RANGE: {
const params = commandInfo.params as IMoveRangeMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.from.subUnitId,
range: new ObjectMatrix(params.from.value).getRange(),
}, {
unitId: params.unitId,
subUnitId: params.to.subUnitId,
range: new ObjectMatrix(params.to.value).getRange(),
}];
}
case SheetValueChangeType.REMOVE_WORKSHEET_MERGE: {
const params = commandInfo.params as IRemoveWorksheetMergeMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range,
}));
}
case SheetValueChangeType.ADD_WORKSHEET_MERGE: {
const params = commandInfo.params as IAddWorksheetMergeMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range,
}));
}
case SheetValueChangeType.REORDER_RANGE: {
const params = commandInfo.params as IReorderRangeMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: params.range,
}];
}
case SheetValueChangeType.SET_WORKSHEET_DEFAULT_STYLE: {
const target = getSheetCommandTarget(univerInstanceService, commandInfo.params as ISetWorksheetDefaultStyleMutationParams);
if (!target) return [];
const { worksheet, unitId, subUnitId } = target;
return [{
unitId,
subUnitId,
range: {
startRow: 0,
endRow: worksheet.getRowCount() - 1,
startColumn: 0,
endColumn: worksheet.getColumnCount() - 1,
},
}];
}
case SheetValueChangeType.SET_ROW_DATA: {
const target = getSheetCommandTarget(univerInstanceService, commandInfo.params as ISetRowDataMutationParams);
if (!target) return [];
const { worksheet, unitId, subUnitId } = target;
const { rowData } = commandInfo.params as ISetRowDataMutationParams;
const rowIndices = Object.keys(rowData).map(Number);
if (rowIndices.length === 0) return [];
return [{
unitId,
subUnitId,
range: {
startRow: Math.min(...rowIndices),
endRow: Math.max(...rowIndices),
startColumn: 0,
endColumn: worksheet.getColumnCount() - 1,
},
}];
}
case SheetValueChangeType.SET_COL_DATA: {
const target = getSheetCommandTarget(univerInstanceService, commandInfo.params as ISetColDataMutationParams);
if (!target) return [];
const { worksheet, unitId, subUnitId } = target;
const { columnData } = commandInfo.params as ISetColDataMutationParams;
const colIndices = Object.keys(columnData).map(Number);
if (colIndices.length === 0) return [];
return [{
unitId,
subUnitId,
range: {
startRow: 0,
endRow: worksheet.getRowCount() - 1,
startColumn: Math.min(...colIndices),
endColumn: Math.max(...colIndices),
},
}];
}
case SheetValueChangeType.SET_WORKSHEET_RANGE_THEME_STYLE:
case SheetValueChangeType.DELETE_WORKSHEET_RANGE_THEME_STYLE: {
const params = commandInfo.params as IWorksheetRangeThemeStyleMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: params.range,
}];
}
default:
return [];
}
}
/**
* Get the affected range for skeleton change commands
* @param {ICommandInfo} commandInfo The command information
* @returns {{ unitId: string; subUnitId: string; range: IRange }[]} Array of affected ranges
*/
// eslint-disable-next-line max-lines-per-function
export function getSkeletonChangedEffectedRange(commandInfo: ICommandInfo, columnCount: number): { unitId: string; subUnitId: string; range: IRange }[] {
switch (commandInfo.id) {
case SheetSkeletonChangeType.SET_WORKSHEET_ROW_HEIGHT:
case SheetSkeletonChangeType.SET_WORKSHEET_ROW_IS_AUTO_HEIGHT: {
const params = commandInfo.params as ISetWorksheetRowHeightMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...range,
rangeType: RANGE_TYPE.ROW,
},
}));
}
// Note: SET_WORKSHEET_ROW_AUTO_HEIGHT has no ranges
case SheetSkeletonChangeType.SET_WORKSHEET_ROW_AUTO_HEIGHT:{
const params = commandInfo.params as ISetWorksheetRowAutoHeightMutationParams;
return params.rowsAutoHeightInfo.map((rowAutoHeightInfo) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
startRow: rowAutoHeightInfo.row,
endRow: rowAutoHeightInfo.row,
startColumn: 0,
endColumn: columnCount - 1,
rangeType: RANGE_TYPE.ROW,
},
}));
}
case SheetSkeletonChangeType.SET_WORKSHEET_COL_WIDTH: {
const params = commandInfo.params as ISetWorksheetColWidthMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...range,
rangeType: RANGE_TYPE.COLUMN,
},
}));
}
case SheetSkeletonChangeType.MOVE_ROWS:
case SheetSkeletonChangeType.MOVE_COLUMNS: {
const params = commandInfo.params as IMoveRowsMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: params.targetRange,
}, {
unitId: params.unitId,
subUnitId: params.subUnitId,
range: params.sourceRange,
}];
}
case SheetSkeletonChangeType.SET_COL_HIDDEN:
case SheetSkeletonChangeType.SET_COL_VISIBLE: {
const params = commandInfo.params as ISetColVisibleMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...range,
rangeType: RANGE_TYPE.COLUMN,
},
}));
}
case SheetSkeletonChangeType.SET_ROW_HIDDEN:
case SheetSkeletonChangeType.SET_ROW_VISIBLE: {
const params = commandInfo.params as ISetRowVisibleMutationParams;
return params.ranges.map((range) => ({
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...range,
rangeType: RANGE_TYPE.ROW,
},
}));
}
case SheetSkeletonChangeType.INSERT_COL: {
const params = commandInfo.params as IInsertColMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...params.range,
rangeType: RANGE_TYPE.COLUMN,
},
}];
}
case SheetSkeletonChangeType.INSERT_ROW: {
const params = commandInfo.params as IInsertRowMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...params.range,
rangeType: RANGE_TYPE.ROW,
},
}];
}
case SheetSkeletonChangeType.REMOVE_COL: {
const params = commandInfo.params as IRemoveColMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...params.range,
rangeType: RANGE_TYPE.COLUMN,
},
}];
}
case SheetSkeletonChangeType.REMOVE_ROW: {
const params = commandInfo.params as IRemoveRowsMutationParams;
return [{
unitId: params.unitId,
subUnitId: params.subUnitId,
range: {
...params.range,
rangeType: RANGE_TYPE.ROW,
},
}];
}
case SheetSkeletonChangeType.TOGGLE_GRIDLINES:
case SheetSkeletonChangeType.SET_GRIDLINES_COLOR: {
return [];
}
default:
return [];
}
}

View File

@@ -0,0 +1,21 @@
/**
* 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 { IUniverSheetsConfig } from '../interfaces/spreadsheet-config';
export const DEFAULT_SPREADSHEET_PLUGIN_DATA: IUniverSheetsConfig = {
selections: {},
};

View File

@@ -0,0 +1,18 @@
/**
* 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.
*/
export * from './command-listener-const';
export * from './default-spreadsheet-plugin-data';

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 { IUniverInstanceService, Workbook } from '@univerjs/core';
import type { IDefinedNamesService, IFunctionService, ISuperTableService } from '@univerjs/engine-formula';
import { Tools, UniverInstanceType } from '@univerjs/core';
import { isReferenceStringWithEffectiveColumn } from '@univerjs/engine-formula';
import { cjk } from '@univerjs/engine-render';
interface IValidateDefinedNameOptions {
unitId: string;
formulaOrRefString: string;
univerInstanceService: IUniverInstanceService;
definedNamesService: IDefinedNamesService;
superTableService: ISuperTableService;
functionService: IFunctionService;
id?: string;
}
export function validateDefinedName(name: string, options: IValidateDefinedNameOptions): true | string {
if (name.length === 0) {
return 'sheets.definedName.nameEmpty';
}
const { unitId, formulaOrRefString, univerInstanceService, definedNamesService, superTableService, functionService, id } = options;
/**
* The defined name can't be duplicate with existing defined names.
* If id is provided, it means we are updating an existing defined name. We should allow the name to be the same as itself.
*/
const existingDefinedName = definedNamesService.getValueByName(unitId, name);
if (
existingDefinedName &&
(id === null || id === undefined || id.length === 0 || existingDefinedName.id !== id)
) {
return 'sheets.definedName.nameDuplicate';
}
// The defined name can't be duplicate with existing table names.
if (superTableService.hasTable(unitId, name)) {
return 'sheets.definedName.nameDuplicate';
}
// The defined name can't be a reference string with effective column, which will cause confusion.
if (
!Tools.isValidParameter(name) ||
isReferenceStringWithEffectiveColumn(name) ||
(!Tools.isStartValidPosition(name) && !cjk.hasCJKText(name.substring(0, 1)))
) {
return 'sheets.definedName.nameInvalid';
}
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
throw new Error(`Workbook not found for unitId: ${unitId}`);
}
const worksheets = workbook.getSheets();
// The defined name can't be duplicate with existing sheet names.
if (worksheets.some((sheet) => sheet.getName() === name)) {
return 'sheets.definedName.nameSheetConflict';
}
if (formulaOrRefString.length === 0) {
return 'sheets.definedName.formulaOrRefStringEmpty';
}
// The defined name can't be duplicate with existing function names.
if (functionService.hasExecutor(name.toUpperCase())) {
return 'sheets.definedName.nameConflict';
}
return true;
}

View File

@@ -0,0 +1,558 @@
/**
* 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, IRange, Worksheet } from '@univerjs/core';
import { createRowColIter, ObjectMatrix, Rectangle } from '@univerjs/core';
export interface IExpandParams {
left?: boolean;
right?: boolean;
up?: boolean;
down?: boolean;
}
type IMatrixWithSpanInfo = ICellData & {
spanAnchor?: {
startRow: number;
startColumn: number;
endRow: number;
endColumn: number;
};
};
interface IExpandedRangeResult {
spanAnchor: IRange | null;
hasValue: boolean;
range: IRange;
}
function cellHasValue(cell: ICellData | undefined): boolean {
if (cell === undefined || cell === null) {
return false;
}
return (cell.v !== undefined && cell.v !== null && cell.v !== '') || cell.p !== undefined;
}
function hasValueFromMatrixWithSpanInfo(cell: IMatrixWithSpanInfo | undefined, matrix: ObjectMatrix<IMatrixWithSpanInfo>): boolean {
if (cell && cell.spanAnchor) {
return cellHasValue(matrix.getValue(cell.spanAnchor.startRow, cell.spanAnchor.startColumn)!);
}
return cellHasValue(cell);
}
function getMatrixWithSpanInfo(worksheet: Worksheet, startRow: number, startColumn: number, endRow: number, endColumn: number): ObjectMatrix<IMatrixWithSpanInfo> {
const matrix = worksheet.getCellMatrix();
// get all merged cells
const mergedCellsInRange = worksheet.getSpanModel().getMergedCellRange(startRow, startColumn, endRow, endColumn);
// iterate all cells in the range
const returnCellMatrix = new ObjectMatrix<IMatrixWithSpanInfo>();
matrix.forValue((row, col) => {
const v = matrix.getValue(row, col);
if (v) {
returnCellMatrix.setValue(row, col, v);
}
});
mergedCellsInRange.forEach((mergedCell) => {
const { startColumn, startRow, endColumn, endRow } = mergedCell;
createRowColIter(startRow, endRow, startColumn, endColumn).forEach((row, col) => {
if (row === startRow && col === startColumn) {
returnCellMatrix.setValue(row, col, {
...matrix.getValue(row, col),
spanAnchor: { startRow, endRow, startColumn, endColumn },
});
}
if (row !== startRow || col !== startColumn) {
returnCellMatrix.realDeleteValue(row, col);
returnCellMatrix.setValue(row, col, {
spanAnchor: { startRow, endRow, startColumn, endColumn },
});
}
});
});
return returnCellMatrix;
}
// do not delete following code, it may be used in the future
// function getMergeRange(startRow: number, startColumn: number, endRow: number, endColumn: number, allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>): IRange {
// let isSearching = true;
// let left = startColumn;
// let right = endColumn;
// let up = startRow;
// let down = endRow;
// while (isSearching) {
// isSearching = false;
// // left
// for (let i = startRow; i < endRow; i++) {
// const cell = allMatrixWithSpan.getValue(i, startColumn);
// if (cell && cell.spanAnchor) {
// left = Math.min(left, cell.spanAnchor.startColumn);
// right = Math.max(right, cell.spanAnchor.endColumn);
// up = Math.min(up, cell.spanAnchor.startRow);
// down = Math.max(down, cell.spanAnchor.endRow);
// }
// }
// // right
// for (let i = startRow; i < endRow; i++) {
// const cell = allMatrixWithSpan.getValue(i, endColumn);
// if (cell && cell.spanAnchor) {
// left = Math.min(left, cell.spanAnchor.startColumn);
// right = Math.max(right, cell.spanAnchor.endColumn);
// up = Math.min(up, cell.spanAnchor.startRow);
// down = Math.max(down, cell.spanAnchor.endRow);
// }
// }
// // up
// for (let i = startColumn; i < endColumn; i++) {
// const cell = allMatrixWithSpan.getValue(startRow, i);
// if (cell && cell.spanAnchor) {
// left = Math.min(left, cell.spanAnchor.startColumn);
// right = Math.max(right, cell.spanAnchor.endColumn);
// up = Math.min(up, cell.spanAnchor.startRow);
// down = Math.max(down, cell.spanAnchor.endRow);
// }
// }
// // down
// for (let i = startColumn; i < endColumn; i++) {
// const cell = allMatrixWithSpan.getValue(endRow, i);
// if (cell && cell.spanAnchor) {
// left = Math.min(left, cell.spanAnchor.startColumn);
// right = Math.max(right, cell.spanAnchor.endColumn);
// up = Math.min(up, cell.spanAnchor.startRow);
// down = Math.max(down, cell.spanAnchor.endRow);
// }
// }
// if (startColumn !== left || startRow !== up || endColumn !== right || endRow !== down) {
// isSearching = true;
// startColumn = left;
// startRow = up;
// endColumn = right;
// endRow = down;
// }
// }
// return {
// startRow: up,
// startColumn: left,
// endRow: down,
// endColumn: right,
// };
// }
function getExpandedRangeLeft(range: IRange, allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>, leftOffset: number, isWorksheetHasSpan: boolean): IExpandedRangeResult {
const { startRow, startColumn, endRow } = range;
let spanAnchor: IRange | null = null;
let hasValue = false;
for (let i = startRow; i <= endRow; i++) {
const cell = allMatrixWithSpan.getValue(i, startColumn - leftOffset)!;
hasValue = hasValue || hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan);
if (!isWorksheetHasSpan && hasValue) {
break;
}
if (cell && cell.spanAnchor) {
if (!spanAnchor) {
spanAnchor = {
startRow: cell.spanAnchor.startRow,
startColumn: cell.spanAnchor.startColumn,
endRow: cell.spanAnchor.endRow,
endColumn: cell.spanAnchor.endColumn,
};
} else {
spanAnchor = {
startRow: Math.min(cell.spanAnchor.startRow, spanAnchor.startRow),
startColumn: Math.min(cell.spanAnchor.startColumn, spanAnchor.startColumn),
endRow: Math.max(cell.spanAnchor.endRow, spanAnchor.endRow),
endColumn: Math.max(cell.spanAnchor.endColumn, spanAnchor.endColumn),
};
}
}
}
if (hasValue) {
range.startColumn = range.startColumn - leftOffset;
return {
spanAnchor,
hasValue: true,
range,
};
}
if (spanAnchor) {
return {
spanAnchor: Rectangle.simpleRangesIntersect(range, spanAnchor) ? spanAnchor : null,
hasValue: false,
range,
};
}
return {
spanAnchor: null,
hasValue: false,
range,
};
}
function getExpandedRangeRight(range: IRange, allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>, rightOffset: number, isWorksheetHasSpan: boolean): IExpandedRangeResult {
const { startRow, endColumn, endRow } = range;
let spanAnchor: IRange | null = null;
let hasValue = false;
for (let i = startRow; i <= endRow; i++) {
const cell = allMatrixWithSpan.getValue(i, endColumn + rightOffset)!;
hasValue = hasValue || hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan);
if (!isWorksheetHasSpan && hasValue) {
break;
}
if (cell && cell.spanAnchor) {
if (!spanAnchor) {
spanAnchor = {
startRow: cell.spanAnchor.startRow,
startColumn: cell.spanAnchor.startColumn,
endRow: cell.spanAnchor.endRow,
endColumn: cell.spanAnchor.endColumn,
};
} else {
spanAnchor = {
startRow: Math.min(cell.spanAnchor.startRow, spanAnchor.startRow),
startColumn: Math.min(cell.spanAnchor.startColumn, spanAnchor.startColumn),
endRow: Math.max(cell.spanAnchor.endRow, spanAnchor.endRow),
endColumn: Math.max(cell.spanAnchor.endColumn, spanAnchor.endColumn),
};
}
}
}
if (hasValue) {
range.endColumn = range.endColumn + rightOffset;
return {
spanAnchor,
hasValue: true,
range,
};
}
if (spanAnchor) {
return {
spanAnchor: Rectangle.simpleRangesIntersect(range, spanAnchor) ? spanAnchor : null,
hasValue: false,
range,
};
}
return {
spanAnchor: null,
hasValue: false,
range,
};
}
function getExpandedRangeUp(range: IRange, allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>, upOffset: number, isWorksheetHasSpan: boolean): IExpandedRangeResult {
const { startRow, startColumn, endColumn } = range;
let spanAnchor: IRange | null = null;
let hasValue = false;
for (let i = startColumn; i <= endColumn; i++) {
const cell = allMatrixWithSpan.getValue(startRow - upOffset, i)!;
hasValue = hasValue || hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan);
if (!isWorksheetHasSpan && hasValue) {
break;
}
if (cell && cell.spanAnchor) {
if (!spanAnchor) {
spanAnchor = {
startRow: cell.spanAnchor.startRow,
startColumn: cell.spanAnchor.startColumn,
endRow: cell.spanAnchor.endRow,
endColumn: cell.spanAnchor.endColumn,
};
} else {
spanAnchor = {
startRow: Math.min(cell.spanAnchor.startRow, spanAnchor.startRow),
startColumn: Math.min(cell.spanAnchor.startColumn, spanAnchor.startColumn),
endRow: Math.max(cell.spanAnchor.endRow, spanAnchor.endRow),
endColumn: Math.max(cell.spanAnchor.endColumn, spanAnchor.endColumn),
};
}
}
}
if (hasValue) {
range.startRow = range.startRow - upOffset;
return {
spanAnchor,
hasValue: true,
range,
};
}
if (spanAnchor) {
return {
spanAnchor: Rectangle.simpleRangesIntersect(range, spanAnchor) ? spanAnchor : null,
hasValue: false,
range,
};
}
return {
spanAnchor: null,
hasValue: false,
range,
};
}
function getExpandedRangeDown(range: IRange, allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>, downOffset: number, isWorksheetHasSpan: boolean): IExpandedRangeResult {
const { startColumn, endColumn, endRow } = range;
let spanAnchor: IRange | null = null;
let hasValue = false;
for (let i = startColumn; i <= endColumn; i++) {
const cell = allMatrixWithSpan.getValue(endRow + downOffset, i)!;
hasValue = hasValue || hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan);
if (!isWorksheetHasSpan && hasValue) {
break;
}
if (cell && cell.spanAnchor) {
if (!spanAnchor) {
spanAnchor = {
startRow: cell.spanAnchor.startRow,
startColumn: cell.spanAnchor.startColumn,
endRow: cell.spanAnchor.endRow,
endColumn: cell.spanAnchor.endColumn,
};
} else {
spanAnchor = {
startRow: Math.min(cell.spanAnchor.startRow, spanAnchor.startRow),
startColumn: Math.min(cell.spanAnchor.startColumn, spanAnchor.startColumn),
endRow: Math.max(cell.spanAnchor.endRow, spanAnchor.endRow),
endColumn: Math.max(cell.spanAnchor.endColumn, spanAnchor.endColumn),
};
}
}
}
if (hasValue) {
range.endRow = range.endRow + downOffset;
return {
spanAnchor,
hasValue: true,
range,
};
}
if (spanAnchor) {
return {
spanAnchor: Rectangle.simpleRangesIntersect(range, spanAnchor) ? spanAnchor : null,
hasValue: false,
range,
};
}
return {
spanAnchor: null,
hasValue: false,
range,
};
}
/**
* A fast path to expand range by checking the four corner cells.
*/
// eslint-disable-next-line max-lines-per-function,complexity
function getExpandedRangeByFastPath({
range,
allMatrixWithSpan,
directions,
isWorksheetHasSpan,
maxRow,
maxColumn,
}: {
range: IRange;
allMatrixWithSpan: ObjectMatrix<IMatrixWithSpanInfo>;
directions: IExpandParams;
isWorksheetHasSpan: boolean;
maxRow: number;
maxColumn: number;
}): {
hasValue: boolean;
range: IRange;
} {
const { left, right, up, down } = directions;
const { startRow, startColumn, endRow, endColumn } = range;
let hasValue = false;
// top-left
if (left && up && startRow > 0 && startColumn > 0) {
const cell = allMatrixWithSpan.getValue(startRow - 1, startColumn - 1)!;
if (hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan)) {
if (isWorksheetHasSpan && cell.spanAnchor) {
range.startRow = cell.spanAnchor.startRow;
range.startColumn = cell.spanAnchor.startColumn;
} else {
range.startRow = startRow - 1;
range.startColumn = startColumn - 1;
}
hasValue = true;
}
}
// top-right
if (right && up && startRow > 0 && endColumn < maxColumn - 1) {
const cell = allMatrixWithSpan.getValue(startRow - 1, endColumn + 1)!;
if (hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan)) {
if (isWorksheetHasSpan && cell.spanAnchor) {
range.startRow = cell.spanAnchor.startRow;
range.endColumn = cell.spanAnchor.endColumn;
} else {
range.startRow = startRow - 1;
range.endColumn = endColumn + 1;
}
hasValue = true;
}
}
// bottom-left
if (left && down && endRow < maxRow - 1 && startColumn > 0) {
const cell = allMatrixWithSpan.getValue(endRow + 1, startColumn - 1)!;
if (hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan)) {
if (isWorksheetHasSpan && cell.spanAnchor) {
range.endRow = cell.spanAnchor.endRow;
range.startColumn = cell.spanAnchor.startColumn;
} else {
range.endRow = endRow + 1;
range.startColumn = startColumn - 1;
}
hasValue = true;
}
}
// bottom-right
if (right && down && endRow < maxRow - 1 && endColumn < maxColumn - 1) {
const cell = allMatrixWithSpan.getValue(endRow + 1, endColumn + 1)!;
if (hasValueFromMatrixWithSpanInfo(cell, allMatrixWithSpan)) {
if (isWorksheetHasSpan && cell.spanAnchor) {
range.endRow = cell.spanAnchor.endRow;
range.endColumn = cell.spanAnchor.endColumn;
} else {
range.endRow = endRow + 1;
range.endColumn = endColumn + 1;
}
hasValue = true;
}
}
return {
hasValue,
range,
};
}
/**
* Expand the range to a continuous range, it uses when Ctrl + A , or only one cell selected to add a pivot table adn so on.
* The demo unit=YSvbxFMCTxugbku-IWNyxQ&type=2&subunit=U_wr1DEF84N_mbesFNmxR in pro.
* The excel behavior rules:
* 1. If the range has a span, the range should expand to whole span range.
* 2. If range left, right, up, down has value, the range should expand to the cell which has value.
* 3. If the range has no value, the range should not expand.
* 4. If the merge has span, the every cell value in span should be the anchor of the span range.
* 5. The span range should be not part in the result range.
* @param {IRange} startRange The start range.
* @param {IExpandParams} directions The directions to expand.
* @param {Worksheet} worksheet The worksheet working on.
* @returns {IRange} The expanded range.
*/
export function expandToContinuousRange(startRange: IRange, directions: IExpandParams, worksheet: Worksheet): IRange {
const maxRow = worksheet.getMaxRows();
const maxColumn = worksheet.getMaxColumns();
const allMatrixWithSpan = getMatrixWithSpanInfo(worksheet, 0, 0, maxRow - 1, maxColumn - 1);
const worksheetHasSpan = worksheet.getSnapshot().mergeData.length > 0;
const { left, right, up, down } = directions;
let changed = true;
let destRange = { ...startRange };
const spanAnchors: IRange[] = [];
while (changed) {
changed = false;
// try fast path first
const fastPathResult = getExpandedRangeByFastPath({ range: destRange, allMatrixWithSpan, directions, isWorksheetHasSpan: worksheetHasSpan, maxRow, maxColumn });
if (fastPathResult.hasValue) {
destRange = fastPathResult.range;
changed = true;
continue;
}
// then try normal path
if (up && destRange.startRow !== 0) {
const { hasValue, range, spanAnchor } = getExpandedRangeUp(destRange, allMatrixWithSpan, 1, worksheetHasSpan);
if (spanAnchor) {
spanAnchors.push(spanAnchor);
}
if (hasValue) {
destRange = range;
changed = true;
continue;
}
}
if (down && destRange.endRow !== maxRow - 1) {
const { hasValue, range, spanAnchor } = getExpandedRangeDown(destRange, allMatrixWithSpan, 1, worksheetHasSpan);
if (spanAnchor) {
spanAnchors.push(spanAnchor);
}
if (hasValue) {
destRange = range;
changed = true;
continue;
}
}
if (left && destRange.startColumn !== 0) {
const { hasValue, range, spanAnchor } = getExpandedRangeLeft(destRange, allMatrixWithSpan, 1, worksheetHasSpan);
if (spanAnchor) {
spanAnchors.push(spanAnchor);
}
if (hasValue) {
destRange = range;
changed = true;
continue;
}
}
if (right && destRange.endColumn !== maxColumn - 1) {
const { hasValue, range, spanAnchor } = getExpandedRangeRight(destRange, allMatrixWithSpan, 1, worksheetHasSpan);
if (spanAnchor) {
spanAnchors.push(spanAnchor);
}
if (hasValue) {
destRange = range;
changed = true;
continue;
}
}
}
if (spanAnchors.length > 0) {
destRange = Rectangle.union(destRange, ...spanAnchors);
}
return destRange;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
export * from './cell-position';
export * from './const/index';
export * from './interfaces/index';
export * from './selection';
export { createUniqueKey, groupByKey } from './utils';

View File

@@ -0,0 +1,24 @@
/**
* 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 { BooleanNumber } from '@univerjs/core';
/**
* Column ruler data type
*/
export interface IColumnRulerData {
hd: BooleanNumber;
}

View File

@@ -0,0 +1,19 @@
/**
* 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.
*/
export * from './mutation-interface';
export * from './selection-config';
export * from './spreadsheet-config';

View File

@@ -0,0 +1,111 @@
/**
* 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 {
Dimension,
ICellData,
IColumnData,
IObjectArrayPrimitiveType,
IObjectMatrixPrimitiveType,
IRange,
IRowData,
IStyleData,
IWorksheetData,
Nullable,
} from '@univerjs/core';
/** Params of `RemoveSheetMutation` */
export interface IRemoveSheetMutationParams {
subUnitId: string;
unitId: string;
subUnitName: string;
}
/** Params of `InsertSheetMutation` */
export interface IInsertSheetMutationParams {
index: number;
sheet: IWorksheetData;
unitId: string;
styles?: Record<string, Nullable<IStyleData>>;
}
/** Params of InsertRowMutation */
export interface IInsertRowMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
rowInfo?: IObjectArrayPrimitiveType<IRowData>;
}
/** Params of InsertColMutation */
export interface IInsertColMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
colInfo?: IObjectArrayPrimitiveType<IColumnData>;
}
/** Params of InsertRowMutation */
export interface IRemoveRowsMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
}
/** Params of InsertColMutation */
export interface IRemoveColMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
}
/** Params of DeleteRange */
export interface IDeleteRangeMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
shiftDimension: Dimension;
}
/** Params of InsertRange */
export interface IInsertRangeMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
shiftDimension: Dimension;
cellValue?: IObjectMatrixPrimitiveType<Nullable<ICellData>>;
}
/** Params of RemoveWorksheetMergeMutation */
export interface IRemoveWorksheetMergeMutationParams {
unitId: string;
subUnitId: string;
ranges: IRange[];
}
/** Params of AddWorksheetMergeMutation */
export interface IAddWorksheetMergeMutationParams {
unitId: string;
subUnitId: string;
ranges: IRange[];
}
/** Params of AddWorksheetMergeMutation */
export interface IWorksheetRangeThemeStyleMutationParams {
unitId: string;
subUnitId: string;
range: IRange;
themeName: string;
}

View File

@@ -0,0 +1,31 @@
/**
* 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 { IRange, IRangeCellData } from '@univerjs/core';
export interface ISelectionConfig {
selection: IRange;
cell?: IRangeCellData;
}
export interface ISelectionsConfig {
[subUnitId: string]: ISelectionConfig[];
}
export interface IDiscreteRange {
rows: number[];
cols: number[];
}

View File

@@ -0,0 +1,21 @@
/**
* 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 { ISelectionsConfig } from './selection-config';
export interface IUniverSheetsConfig {
selections: ISelectionsConfig;
}

View File

@@ -0,0 +1,162 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRange } from '@univerjs/core';
import { ObjectMatrix, Range } from '@univerjs/core';
export const createTopMatrixFromRanges = (ranges: IRange[]) => {
const matrix = new ObjectMatrix<number>();
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
matrix.setValue(row, col, 1);
});
});
matrix.forValue((row, col) => {
const theLastRowValue = matrix.getValue(row - 1, col);
if (theLastRowValue) {
matrix.setValue(row, col, theLastRowValue + 1);
}
});
return matrix;
};
export const createTopMatrixFromMatrix = (matrix: ObjectMatrix<1>) => {
const _matrix = matrix as ObjectMatrix<number>;
_matrix.forValue((row, col) => {
const theLastRowValue = matrix.getValue(row - 1, col);
if (theLastRowValue) {
_matrix.setValue(row, col, theLastRowValue + 1);
}
});
return _matrix;
};
const findMaximalRectangle = (topMatrix: ObjectMatrix<number>) => {
const res: {
area: number;
range?: IRange;
} = {
area: 0,
};
const checkArea = (area: number, range: IRange) => {
if (res.area < area) {
res.area = area;
res.range = range;
return true;
}
return false;
};
topMatrix.forValue((row, col, lineArea) => {
let cols = 1;
let rows = lineArea;
checkArea(cols * rows, {
startRow: row - rows + 1,
endRow: row,
startColumn: col,
endColumn: col,
});
const _range = {
startRow: row - rows + 1,
endRow: row,
startColumn: 0,
endColumn: col,
};
for (let k = col - 1; k >= 0; k--) {
if (!topMatrix.getValue(row, k)) {
break;
} else {
rows = Math.min(topMatrix.getValue(row, k) || 0, rows);
cols++;
const area = rows * cols;
_range.startColumn = k;
_range.startRow = row - rows + 1;
checkArea(area, _range);
}
}
});
return res;
};
const filterLeftMatrix = (topMatrix: ObjectMatrix<number>, range: IRange) => {
Range.foreach(range, (row, col) => {
topMatrix.realDeleteValue(row, col);
});
for (let col = range.startColumn; col <= range.endColumn; col++) {
const row = range.endRow + 1;
const v = topMatrix.getValue(row, col)!;
if (v > 0) {
topMatrix.setValue(row, col, 1);
let nextRow = row + 1;
while (topMatrix.getValue(nextRow, col)! > 0) {
topMatrix.setValue(nextRow, col, topMatrix.getValue(nextRow - 1, col)! + 1);
nextRow++;
}
}
}
return topMatrix;
};
export const findAllRectangle = (topMatrix: ObjectMatrix<number>) => {
const resultList = [];
let result = findMaximalRectangle(topMatrix);
while (result.area > 0) {
if (result.range) {
resultList.push(result.range);
filterLeftMatrix(topMatrix, result.range);
}
result = findMaximalRectangle(topMatrix);
}
return resultList;
};
/**
* Some operations generate sparse ranges such as paste/autofill/ref-range, and this function merge some small ranges into some large ranges to reduce transmission size.
* Time Complexity: O(mn) , where m and n are rows and columns. It takes O(mn) to compute the markMatrix and O(n) to apply the histogram algorithm to each column.
* ps. column sparse matrices have better performance
* @param {IRange[]} ranges
* @returns {IRange[]}
*/
export const rangeMerge = (ranges: IRange[]) => {
const topMatrix = createTopMatrixFromRanges(ranges);
return findAllRectangle(topMatrix);
};
export class RangeMergeUtil {
private _matrix = new ObjectMatrix<1>();
add(...ranges: IRange[]) {
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
this._matrix.setValue(row, col, 1);
});
});
return this;
}
subtract(...ranges: IRange[]) {
ranges.forEach((range) => {
Range.foreach(range, (row, col) => {
this._matrix.realDeleteValue(row, col);
});
});
return this;
}
merge() {
const topMatrix = createTopMatrixFromMatrix(this._matrix);
const ranges = findAllRectangle(topMatrix);
return ranges;
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IColumnData, IRowData, Nullable } from '@univerjs/core';
import { Tools } from '@univerjs/core';
/**
* Reset the row data to undefined when undoing the operation
* @param currentRow
* @returns
*/
export function getOldRowData(currentRow: Nullable<Partial<IRowData>>, newRow: Nullable<Partial<IRowData>>): Nullable<Partial<IRowData>> {
if (currentRow === null || currentRow === undefined) {
return currentRow;
}
const row = Tools.deepClone(currentRow);
if (newRow === null || newRow === undefined) {
return row;
}
const oldRow: Partial<IRowData> = {};
// Restore only changed properties
if ('h' in newRow) {
oldRow.h = row.h;
}
if ('ia' in newRow) {
oldRow.ia = row.ia;
}
if ('ah' in newRow) {
oldRow.ah = row.ah;
}
if ('hd' in newRow) {
oldRow.hd = row.hd;
}
if ('s' in newRow) {
oldRow.s = row.s;
}
if ('custom' in newRow) {
oldRow.custom = row.custom;
}
return oldRow;
}
/**
* Reset the column data to undefined when undoing the operation
* @param currenColumn
* @param newColumn
* @returns
*/
export function getOldColumnData(currenColumn: Nullable<Partial<IColumnData>>, newColumn: Nullable<Partial<IColumnData>>): Nullable<Partial<IColumnData>> {
if (currenColumn === null || currenColumn === undefined) {
return currenColumn;
}
const column = Tools.deepClone(currenColumn);
if (newColumn === null || newColumn === undefined) {
return column;
}
const oldColumn: Partial<IColumnData> = {};
// Restore only changed properties
if ('w' in newColumn) {
oldColumn.w = column.w;
}
if ('hd' in newColumn) {
oldColumn.hd = column.hd;
}
if ('s' in newColumn) {
oldColumn.s = column.s;
}
if ('custom' in newColumn) {
oldColumn.custom = column.custom;
}
return oldColumn;
}

View File

@@ -0,0 +1,212 @@
/**
* 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 {
ICellWithCoord,
IRange,
IRangeWithCoord,
ISelection,
ISelectionCell,
Nullable,
} from '@univerjs/core';
export const SELECTION_CONTROL_BORDER_BUFFER_WIDTH = 1.5; // The draggable range of the selection is too thin, making it easy for users to miss. Therefore, a buffer gap is provided to make it easier for users to select.
export const SELECTION_CONTROL_BORDER_BUFFER_COLOR = 'rgba(255, 255, 255, 0.01)';
/**
* Whether to display the controller that modifies the selection, distributed in 8 locations
* tl top_left_corner
* tc top_center_corner
* tr top_right_corner
* ml middle_left_corner
* mr middle_right_corner
* bl bottom_left_corner
* bc bottom_center_corner
* br bottom_right_corner
*/
export interface ISelectionWidgetConfig {
tl?: boolean;
tc?: boolean;
tr?: boolean;
ml?: boolean;
mr?: boolean;
bl?: boolean;
bc?: boolean;
br?: boolean;
}
/**
* https://support.microsoft.com/en-us/office/select-cell-contents-in-excel-23f64223-2b6b-453a-8688-248355f10fa9
*/
export interface ISelectionStyle {
/**
* Assign an ID to a selection area.
* The current scenario is to identify the formula string corresponding to the selection area
*/
id?: string;
/**
* The volume of the selection border determines the thickness of the selection border
*/
strokeWidth: number;
/**
* The color of the selection border.
*/
stroke: string;
/**
* The dashed line of the selection border. Here, the dashed line is a numerical value, different from the canvas dashed line setting. It is implemented internally as [0, strokeDash]. Setting it to 8 will look more aesthetically pleasing.
*/
strokeDash?: number;
/**
* Enable 'marching ants' animation
*/
isAnimationDash?: boolean;
/**
* The fill color inside the selection. It needs to have a level of transparency, otherwise content in the covered area of the selection will be obscured.
*/
fill: string;
/**
* The eight touch points of the selection. You can refer to Excel's formula and chart selections,
* which allow you to manually adjust the size of the selection.
* Univer has four more touch points (up, down, left, and right) than Excel.
* https://support.microsoft.com/en-us/office/select-data-for-a-chart-5fca57b7-8c52-4e09-979a-631085113862
*/
widgets: ISelectionWidgetConfig;
/**
* The volume of the touch points.
*/
widgetSize: number;
/**
* The thickness of the border of the touch points
*/
widgetStrokeWidth?: number;
/**
* The color of the touch points.
*/
widgetStroke?: string;
/**
* https://support.microsoft.com/en-us/office/copy-a-formula-by-dragging-the-fill-handle-in-excel-for-mac-dd928259-622b-473f-9a33-83aa1a63e218
* Whether to show the drop-down fill button at the bottom right corner of the selection.
*/
// hasAutoFill?: boolean;
autofillSize?: number; // The size of the fill button.
autofillStrokeWidth?: number; // The border size of the fill button.
autofillStroke?: string; // The color of the fill button.
/**
* The color of the row title highlight.
* A level of transparency should be set to avoid covering the row title content.
*/
rowHeaderFill?: string;
/**
* The color of the bottom border of the row title.
*/
rowHeaderStroke?: string;
/**
* The color of the bottom border of the row title.
*/
rowHeaderStrokeWidth?: number;
columnHeaderFill?: string;
columnHeaderStroke?: string;
columnHeaderStrokeWidth?: number;
expandCornerSize?: number;
}
/**
* Selection range Info, contains selection range & primary range
* primary range is the range of the highlighted cell.
*
* rangeWithCoord: IRangeWithCoord;
* primaryWithCoord: Nullable<ICellWithCoord>;
* style?
*/
export interface ISelectionWithCoord {
rangeWithCoord: IRangeWithCoord;
primaryWithCoord: Nullable<ICellWithCoord>;
style?: Nullable<ISelectionStyle>;
}
/**
* range: IRange;
* primary: Nullable<ISelectionCell>;
* style: Nullable<ISelectionStyle>;
*/
export interface ISelectionWithStyle extends ISelection {
range: IRange;
/**
* if primary is null, means clear primary cell.
* if primary is not defined, means not keep current primary cell.
*/
primary: Nullable<ISelectionCell>;
style?: Nullable<Partial<ISelectionStyle>>;
}
export interface ISheetRangeLocation {
range: IRange;
subUnitId: string;
unitId: string;
}
/**
* Process a selection with coordinates and style,
* and extract the coordinate information, because the render needs coordinates when drawing.
* Since the selection.manager.service is unrelated to the coordinates,
* it only accepts data of type ISelectionWithStyle, so a conversion is necessary.
* @param selectionWithCoordAndStyle Selection with coordinates and style
* @returns
*/
export function convertSelectionDataToRange(
selectionWithCoordAndStyle: ISelectionWithCoord
): ISelectionWithStyle {
const { rangeWithCoord, primaryWithCoord, style } = selectionWithCoordAndStyle;
const result: ISelectionWithStyle = {
range: {
startRow: rangeWithCoord.startRow,
startColumn: rangeWithCoord.startColumn,
endRow: rangeWithCoord.endRow,
endColumn: rangeWithCoord.endColumn,
rangeType: rangeWithCoord.rangeType,
unitId: rangeWithCoord.unitId,
sheetId: rangeWithCoord.sheetId,
},
primary: null,
style,
};
if (primaryWithCoord != null) {
result.primary = convertPrimaryWithCoordToPrimary(primaryWithCoord);
}
return result;
}
export function convertPrimaryWithCoordToPrimary(primaryWithCoord: ICellWithCoord) {
const { actualRow, actualColumn, isMerged, isMergedMainCell } = primaryWithCoord;
const { startRow, startColumn, endRow, endColumn } = primaryWithCoord.mergeInfo;
return {
actualRow,
actualColumn,
isMerged,
isMergedMainCell,
startRow,
startColumn,
endRow,
endColumn,
};
}

View File

@@ -0,0 +1,29 @@
/**
* 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 { Worksheet } from '@univerjs/core';
export function rowHeightByHeader(worksheet: Worksheet) {
const config = worksheet?.getConfig();
const columnHeader = config?.columnHeader.height || 0;
return columnHeader;
}
export function columnWidthByHeader(worksheet: Worksheet) {
const config = worksheet?.getConfig();
const rowHeader = config?.rowHeader.width || 0;
return rowHeader;
}

View File

@@ -0,0 +1,237 @@
/**
* 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, IDocumentData, IRange, Nullable, Worksheet } from '@univerjs/core';
import { CellValueType, Range, regexp } from '@univerjs/core';
/**
* The default delimiter to split the text.
*/
export enum SplitDelimiterEnum {
/**
* The tab character
*/
Tab = 1,
/**
* The comma character
*/
Comma = 2,
/**
* The semicolon character
*/
Semicolon = 4,
/**
* The space character
*/
Space = 8,
/**
* The custom delimiter
*/
Custom = 16,
}
class DelimiterCounter {
private _tabCount = 0;
private _commaCount = 0;
private _semicolonCount = 0;
private _spaceCount = 0;
add(delimiter: string) {
switch (delimiter) {
case '\t':
this._tabCount++;
break;
case ',':
this._commaCount++;
break;
case ';':
this._semicolonCount++;
break;
case ' ':
this._spaceCount++;
break;
default:
break;
}
}
update(cellText: string | undefined) {
if (cellText && typeof cellText === 'string') {
if (cellText.includes('\t')) {
this._tabCount++;
}
if (cellText.includes(',')) {
this._commaCount++;
}
if (cellText.includes(';')) {
this._semicolonCount++;
}
if (cellText.trim().includes(' ')) {
this._spaceCount++;
}
}
}
getDelimiter() {
const maxCount = Math.max(this._tabCount, this._commaCount, this._semicolonCount, this._spaceCount);
if (maxCount === 0) {
return SplitDelimiterEnum.Tab;
}
if (maxCount === this._tabCount) {
return SplitDelimiterEnum.Tab;
}
if (maxCount === this._commaCount) {
return SplitDelimiterEnum.Comma;
}
if (maxCount === this._semicolonCount) {
return SplitDelimiterEnum.Semicolon;
}
if (maxCount === this._spaceCount) {
return SplitDelimiterEnum.Space;
}
return SplitDelimiterEnum.Tab;
}
}
function getDelimiterRegexItem(delimiter: SplitDelimiterEnum, treatMultipleDelimitersAsOne: boolean, customDelimiter?: string) {
const delimiterList = [];
if (customDelimiter !== undefined && (delimiter & SplitDelimiterEnum.Custom) > 0) {
delimiterList.push(customDelimiter);
}
if ((delimiter & SplitDelimiterEnum.Tab) > 0) {
delimiterList.push('\t');
}
if ((delimiter & SplitDelimiterEnum.Comma) > 0) {
delimiterList.push(',');
}
if ((delimiter & SplitDelimiterEnum.Semicolon) > 0) {
delimiterList.push(';');
}
if ((delimiter & SplitDelimiterEnum.Space) > 0) {
delimiterList.push(' ');
}
let str = '';
for (const delimiter of delimiterList) {
str += regexp.escapeRegExp(delimiter);
}
let allStr = '['.concat(str, ']');
if (treatMultipleDelimitersAsOne) {
allStr += '+';
}
return new RegExp(allStr);
}
const getStringFromDataStream = (data: IDocumentData): string => {
const dataStream = data.body?.dataStream.replace(/\r\n$/, '') || '';
return dataStream;
};
function cellValueToString(cellData: Nullable<ICellData>) {
if (cellData === null || cellData === undefined) {
return undefined;
}
if (cellData.p) {
return getStringFromDataStream(cellData.p);
}
if (cellData.v && typeof cellData.v === 'string') {
return cellData.v;
}
if (cellData.t && (cellData.t === CellValueType.FORCE_STRING || cellData.t === CellValueType.STRING)) {
return String(cellData.v);
}
return undefined;
}
interface ISplitRangeTextResult {
rs: (string[] | undefined)[];
maxLength: number;
lastRow: number;
}
/**
* Split the text in the range into a two-dimensional array.
* @param {Worksheet} sheet The worksheet which range belongs to.
* @param {IRange} range The range to split.
* @param {SplitDelimiterEnum} [delimiter] The delimiter to split the text.
* @param {string} [customDelimiter] The custom delimiter to split the text. An error will be thrown if customDelimiter is not a character.
* @param {boolean} [treatMultipleDelimitersAsOne] split multiple delimiters as one.
* @returns {ISplitRangeTextResult} The two-dimensional array of the split text and max column length.
*/
export function splitRangeText(sheet: Worksheet, range: IRange, delimiter?: SplitDelimiterEnum, customDelimiter?: string, treatMultipleDelimitersAsOne: boolean = false): ISplitRangeTextResult {
const sourceRange = Range.transformRange(range, sheet);
const { startColumn, startRow, endColumn, endRow } = sourceRange;
if (startColumn !== endColumn) {
throw new Error('The range must be in the same column.');
}
if ((delimiter && (delimiter & SplitDelimiterEnum.Custom) > 0) && (customDelimiter === undefined || customDelimiter.length !== 1)) {
throw new Error('The custom delimiter must a character.');
}
const needAutoDelimiter = delimiter === undefined;
const delimiterCounter = needAutoDelimiter ? new DelimiterCounter() : null;
const textList = [];
// collect delimiter and text values
for (let i = startRow; i <= endRow; i++) {
const cell = sheet.getCell(i, startColumn);
const cellString = cellValueToString(cell);
textList.push(cellString);
if (delimiterCounter) {
delimiterCounter.update(cellString);
}
}
const useDelimiter = needAutoDelimiter ? delimiterCounter!.getDelimiter() : delimiter;
const useDelimiterRegex = getDelimiterRegexItem(useDelimiter, treatMultipleDelimitersAsOne, customDelimiter);
let maxLength = -1;
let lastRow = 0;
let index = 0;
const rs = [];
for (const text of textList) {
if (text !== undefined) {
const cols = String(text).split(useDelimiterRegex);
if (maxLength < 0) {
maxLength = cols.length;
} else {
maxLength = Math.max(maxLength, cols.length);
}
rs.push(cols);
lastRow = index;
} else {
rs.push(undefined);
}
index++;
}
return {
rs,
maxLength: maxLength === -1 ? 0 : maxLength,
lastRow,
};
}

View File

@@ -0,0 +1,259 @@
/**
* 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, IObjectMatrixPrimitiveType, IRange, Nullable, UniverInstanceService, Workbook, Worksheet } from '@univerjs/core';
import type { IDiscreteRange } from './interfaces';
import { IUniverInstanceService, mergeIntervals, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { getSheetCommandTarget } from '../commands/commands/utils/target-util';
export const groupByKey = <T = Record<string, unknown>>(arr: T[], key: string, blankKey = '') => {
return arr.reduce(
(result, current) => {
const value = current && ((current as Record<string, unknown>)[key] as string);
if (typeof value !== 'string') {
console.warn(current, `${key} is not string`);
return result;
}
if (value) {
if (!result[value]) {
result[value] = [];
}
result[value].push(current);
} else {
result[blankKey].push(current);
}
return result;
},
{} as Record<string, T[]>
);
};
export const createUniqueKey = (initValue = 0) => {
let _initValue = initValue;
/**
* Increments 1 per call
*/
return function getKey() {
return _initValue++;
};
};
function cellHasValue(cell: Nullable<ICellData>): boolean {
if (cell === undefined || cell === null) {
return false;
}
return (cell.v !== undefined && cell.v !== null && cell.v !== '') || cell.p !== undefined;
}
export function findFirstNonEmptyCell(range: IRange, worksheet: Worksheet): IRange | null {
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
const cell = worksheet.getCell(row, col);
if (cellHasValue(cell)) {
return { startRow: row, startColumn: col, endRow: row, endColumn: col };
}
}
}
return null;
}
/**
* Generate cellValue from range and set null
* @param range
* @returns
*/
export function generateNullCell(range: IRange[]): IObjectMatrixPrimitiveType<Nullable<ICellData>> {
const cellValue = new ObjectMatrix<Nullable<ICellData>>();
range.forEach((range: IRange) => {
const { startRow, startColumn, endRow, endColumn } = range;
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
cellValue.setValue(i, j, null);
}
}
});
return cellValue.clone();
}
/**
* Generate cellValue from range and set v/p/f/si/custom to null
* @param range
* @returns
*/
export function generateNullCellValue(range: IRange[]): IObjectMatrixPrimitiveType<ICellData> {
const cellValue = new ObjectMatrix<ICellData>();
range.forEach((range: IRange) => {
const { startRow, startColumn, endRow, endColumn } = range;
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
cellValue.setValue(i, j, {
v: null,
p: null,
f: null,
si: null,
custom: null,
});
}
}
});
return cellValue.clone();
}
// Generate cellValue from range and set s to null
export function generateNullCellStyle(ranges: IRange[]): IObjectMatrixPrimitiveType<ICellData> {
const cellValue = new ObjectMatrix<ICellData>();
ranges.forEach((range: IRange) => {
const { startRow, startColumn, endRow, endColumn } = range;
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
cellValue.setValue(i, j, {
s: null,
});
}
}
});
return cellValue.clone();
}
export function getActiveWorksheet(instanceService: UniverInstanceService): [Nullable<Workbook>, Nullable<Worksheet>] {
const workbook = instanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getActiveSheet();
return [workbook, worksheet];
}
export function discreteRangeToRange(discreteRange: IDiscreteRange): IRange {
const { rows, cols } = discreteRange;
return {
startRow: rows[0],
endRow: rows[rows.length - 1],
startColumn: cols[0],
endColumn: cols[cols.length - 1],
};
}
export function rangeToDiscreteRange(range: IRange, accessor: IAccessor, unitId?: string, subUnitId?: string): IDiscreteRange | null {
const univerInstanceService = accessor.get(IUniverInstanceService);
const workbook = unitId
? univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)
: univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const worksheet = subUnitId ? workbook?.getSheetBySheetId(subUnitId) : workbook?.getActiveSheet();
if (!worksheet) {
return null;
}
const { startRow, endRow, startColumn, endColumn } = range;
const rows = [];
const cols = [];
for (let r = startRow; r <= endRow; r++) {
if (!worksheet.getRowFiltered(r)) {
rows.push(r);
}
}
for (let c = startColumn; c <= endColumn; c++) {
cols.push(c);
}
return {
rows,
cols,
};
}
export function getVisibleRanges(ranges: IRange[], accessor: IAccessor, unitId?: string, subUnitId?: string): IRange[] {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId, subUnitId });
if (!target) {
return ranges;
}
const { worksheet } = target;
const intervalsRanges: Array<{
startColumn: number;
endColumn: number;
rowIntervals: [number, number][];
}> = [];
for (const range of ranges) {
const { startRow, endRow, startColumn, endColumn } = range;
const rowIntervals: [number, number][] = [];
let intervalStartRow = startRow;
for (let r = startRow; r <= endRow; r++) {
if (worksheet.getRowFiltered(r)) {
if (intervalStartRow < r) {
rowIntervals.push([intervalStartRow, r - 1]);
}
intervalStartRow = r + 1;
} else if (r === endRow) {
rowIntervals.push([intervalStartRow, endRow]);
}
}
const findIndex = intervalsRanges.findIndex((item) => item.startColumn === startColumn && item.endColumn === endColumn);
if (findIndex > -1) {
intervalsRanges[findIndex].rowIntervals = intervalsRanges[findIndex].rowIntervals.concat(rowIntervals);
} else {
intervalsRanges.push({
startColumn,
endColumn,
rowIntervals,
});
}
}
const visibleRanges: IRange[] = [];
for (const item of intervalsRanges) {
const { startColumn, endColumn, rowIntervals } = item;
const mergedRowIntervals = mergeIntervals(rowIntervals);
for (const [startRow, endRow] of mergedRowIntervals) {
visibleRanges.push({
startRow,
endRow,
startColumn,
endColumn,
});
}
}
return visibleRanges;
}
export function serializeListOptions(options: string[]) {
return JSON.stringify(options.filter(Boolean));
}
export function deserializeListOptions(optionsStr: string) {
try {
const options = JSON.parse(optionsStr);
if (Array.isArray(options) && options.every((option) => typeof option === 'string')) {
return options.filter(Boolean);
}
} catch {
// Fallback for data saved by older versions.
}
return optionsStr.split(',').filter(Boolean);
}

View File

@@ -0,0 +1,336 @@
/**
* 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, IWorkbookData, Workbook } from '@univerjs/core';
import {
ICommandService,
IConfirmService,
IUniverInstanceService,
LocaleService,
LocaleType,
RANGE_TYPE,
TestConfirmService,
UniverInstanceType,
} from '@univerjs/core';
import { beforeEach, describe, expect, it } from 'vitest';
import enUS from '../../../locale/en-US';
import zhCN from '../../../locale/zh-CN';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import {
addMergeCellsUtil,
AddWorksheetMergeAllCommand,
AddWorksheetMergeCommand,
AddWorksheetMergeHorizontalCommand,
AddWorksheetMergeVerticalCommand,
getMergeableSelectionsByType,
MergeType,
} from '../add-worksheet-merge.command';
import { RemoveWorksheetMergeCommand } from '../remove-worksheet-merge.command';
import { createCommandTestBed } from './create-command-test-bed';
class DeclineConfirmService extends TestConfirmService<unknown> {
override confirm(): Promise<boolean> {
return Promise.resolve(false);
}
}
const WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheetOrder: [
'sheet1',
],
sheets: {
sheet1: {
name: '工作表1',
id: 'sheet1',
tabColor: '',
hidden: 0,
rowCount: 1000,
columnCount: 20,
zoomRatio: 1,
freeze: {
xSplit: 0,
ySplit: 0,
startRow: -1,
startColumn: -1,
},
scrollTop: 0,
scrollLeft: 0,
defaultColumnWidth: 88,
defaultRowHeight: 24,
mergeData: [
{
startRow: 0,
startColumn: 0,
endRow: 0,
endColumn: 8,
rangeType: 0,
unitId: 'test',
sheetId: 'sheet1',
},
{
startRow: 2,
startColumn: 5,
endRow: 3,
endColumn: 5,
rangeType: 0,
unitId: 'test',
sheetId: 'sheet1',
},
],
cellData: {
1: {
5: {
v: 3,
t: 2,
},
},
2: {
5: {
v: 1,
t: 2,
},
},
3: {
5: {},
},
},
rowData: {
1: {
hd: 0,
h: 24,
ah: 24,
},
2: {
hd: 0,
h: 24,
ah: 24,
},
3: {
hd: 0,
h: 24,
ah: 24,
},
},
columnData: {},
showGridlines: 1,
rowHeader: {
width: 46,
hidden: 0,
},
columnHeader: {
height: 20,
hidden: 0,
},
rightToLeft: 0,
},
},
locale: LocaleType.ZH_CN,
name: '',
styles: {},
};
describe('add-merge-command', () => {
let get: Injector['get'];
let injector: Injector;
let commandService: ICommandService;
function getWorksheet(getter: Injector['get'] = get) {
const workbook = getter(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
throw new Error('Expected workbook test to exist.');
}
const worksheet = workbook.getSheetBySheetId('sheet1');
if (!worksheet) {
throw new Error('Expected worksheet sheet1 to exist.');
}
return worksheet;
}
beforeEach(() => {
const testBed = createCommandTestBed(WORKBOOK_DATA_DEMO, [
[IConfirmService, { useClass: TestConfirmService }],
]);
get = testBed.get;
injector = testBed.injector;
commandService = get(ICommandService);
commandService.registerCommand(AddWorksheetMergeCommand);
commandService.registerCommand(AddWorksheetMergeAllCommand);
commandService.registerCommand(AddWorksheetMergeVerticalCommand);
commandService.registerCommand(AddWorksheetMergeHorizontalCommand);
commandService.registerCommand(RemoveWorksheetMergeCommand);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(RemoveWorksheetMergeMutation);
commandService.registerCommand(AddWorksheetMergeMutation);
commandService.registerCommand(SetSelectionsOperation);
get(LocaleService).load({ zhCN, enUS });
});
it('test merge cell contain merge cell', async () => {
await commandService.executeCommand(SetSelectionsOperation.id, {
unitId: 'test',
subUnitId: 'sheet1',
selections: [
{
range: {
rangeType: RANGE_TYPE.NORMAL,
startRow: 1,
endRow: 3,
startColumn: 5,
endColumn: 5,
},
},
],
});
expect(
await commandService.executeCommand(AddWorksheetMergeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
selections: [{ startRow: 1, startColumn: 5, endRow: 3, endColumn: 5 }],
})
).toBeTruthy();
const worksheet = getWorksheet();
const mergeData = worksheet.getConfig().mergeData;
const { startRow, startColumn, endColumn, endRow } = mergeData![0];
expect({
startRow,
startColumn,
endColumn,
endRow,
}).toEqual({
startRow: 0,
startColumn: 0,
endRow: 0,
endColumn: 8,
});
const { startRow: startRow2, startColumn: startColumn2, endColumn: endColumn2, endRow: endRow2 } = mergeData![1];
expect({
startRow: startRow2,
startColumn: startColumn2,
endColumn: endColumn2,
endRow: endRow2,
}).toEqual({
startRow: 1,
startColumn: 5,
endRow: 3,
endColumn: 5,
});
const keptCell = worksheet.getCell(1, 5);
const clearedCell = worksheet.getCell(2, 5);
expect(keptCell && keptCell.v).toBe(3);
expect(clearedCell && clearedCell.v ? clearedCell.v : null).toBeNull();
});
it('keeps existing cells unchanged when the user cancels merging cells with content', async () => {
const testBed = createCommandTestBed(WORKBOOK_DATA_DEMO, [
[IConfirmService, { useClass: DeclineConfirmService }],
]);
const localGet = testBed.get;
const localCommandService = localGet(ICommandService);
localCommandService.registerCommand(AddWorksheetMergeCommand);
localCommandService.registerCommand(SetRangeValuesMutation);
localCommandService.registerCommand(RemoveWorksheetMergeMutation);
localCommandService.registerCommand(AddWorksheetMergeMutation);
localCommandService.registerCommand(SetSelectionsOperation);
localGet(LocaleService).load({ zhCN, enUS });
await expect(localCommandService.executeCommand(AddWorksheetMergeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
selections: [{ startRow: 1, startColumn: 5, endRow: 2, endColumn: 5 }],
defaultMerge: false,
})).resolves.toBe(false);
const worksheet = getWorksheet(localGet);
const firstCell = worksheet.getCell(1, 5);
const secondCell = worksheet.getCell(2, 5);
expect(firstCell && firstCell.v).toBe(3);
expect(secondCell && secondCell.v).toBe(1);
testBed.univer.dispose();
});
it('filters mergeable selections by requested merge type', () => {
const singleCell = { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 };
const oneRow = { startRow: 1, endRow: 1, startColumn: 1, endColumn: 3 };
const oneColumn = { startRow: 2, endRow: 4, startColumn: 2, endColumn: 2 };
const block = { startRow: 5, endRow: 6, startColumn: 5, endColumn: 6 };
expect(getMergeableSelectionsByType(MergeType.MergeAll, null)).toBeNull();
expect(getMergeableSelectionsByType(MergeType.MergeAll, [singleCell, oneRow, oneColumn, block])).toEqual([oneRow, oneColumn, block]);
expect(getMergeableSelectionsByType(MergeType.MergeVertical, [singleCell, oneRow, oneColumn, block])).toEqual([oneColumn, block]);
expect(getMergeableSelectionsByType(MergeType.MergeHorizontal, [singleCell, oneRow, oneColumn, block])).toEqual([oneRow, block]);
});
it('merges current selections through all, vertical, and horizontal commands', async () => {
const selectionManagerService = get(SheetsSelectionsService);
const worksheet = getWorksheet();
selectionManagerService.setSelections([
{
range: { startRow: 5, endRow: 6, startColumn: 0, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
await expect(commandService.executeCommand(AddWorksheetMergeAllCommand.id)).resolves.toBe(true);
expect(worksheet.getMergeData().some((range) => range.startRow === 5 && range.endRow === 6 && range.startColumn === 0 && range.endColumn === 1)).toBe(true);
selectionManagerService.setSelections([
{
range: { startRow: 7, endRow: 8, startColumn: 2, endColumn: 3, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
await expect(commandService.executeCommand(AddWorksheetMergeVerticalCommand.id)).resolves.toBe(true);
expect(worksheet.getMergeData().some((range) => range.startRow === 7 && range.endRow === 8 && range.startColumn === 2 && range.endColumn === 2)).toBe(true);
expect(worksheet.getMergeData().some((range) => range.startRow === 7 && range.endRow === 8 && range.startColumn === 3 && range.endColumn === 3)).toBe(true);
selectionManagerService.setSelections([
{
range: { startRow: 9, endRow: 10, startColumn: 4, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
await expect(commandService.executeCommand(AddWorksheetMergeHorizontalCommand.id)).resolves.toBe(true);
expect(worksheet.getMergeData().some((range) => range.startRow === 9 && range.endRow === 9 && range.startColumn === 4 && range.endColumn === 5)).toBe(true);
expect(worksheet.getMergeData().some((range) => range.startRow === 10 && range.endRow === 10 && range.startColumn === 4 && range.endColumn === 5)).toBe(true);
});
it('force merges through facade utility after removing overlapping merged cells', async () => {
const worksheet = getWorksheet();
const ranges = [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 8 }];
expect(() => addMergeCellsUtil(injector, 'test', 'sheet1', ranges)).toThrow(/overlap/);
addMergeCellsUtil(injector, 'test', 'sheet1', ranges, { isForceMerge: true });
await Promise.resolve();
expect(worksheet.getMergeData().some((range) => range.startRow === 0 && range.endRow === 1 && range.startColumn === 0 && range.endColumn === 8)).toBe(true);
});
});

View File

@@ -0,0 +1,159 @@
/**
* 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 } from '@univerjs/core';
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
import { describe, expect, it, vi } from 'vitest';
import { IAutoFillService } from '../../../services/auto-fill/auto-fill.service';
import { AUTO_FILL_APPLY_TYPE } from '../../../services/auto-fill/type';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AutoFillCommand, SheetCopyDownCommand, SheetCopyRightCommand } from '../auto-fill.command';
function createAccessor(pairs: Array<[unknown, unknown]>): IAccessor {
const map = new Map<unknown, unknown>(pairs);
return {
get(token: unknown) {
if (!map.has(token)) {
throw new Error(`Unknown token: ${String(token)}`);
}
return map.get(token);
},
} as IAccessor;
}
function createCommandAccessor(range?: { startRow: number; endRow: number; startColumn: number; endColumn: number }, executeResult = true) {
const executeCommand = vi.fn(async () => executeResult);
const setShowMenu = vi.fn();
const worksheet = { getSheetId: () => 'sheet-1' };
const workbook = {
getUnitId: () => 'unit-1',
getActiveSheet: () => worksheet,
};
const accessor = createAccessor([
[SheetsSelectionsService, { getCurrentLastSelection: () => (range ? { range } : null) }],
[IUniverInstanceService, { getCurrentUnitOfType: () => workbook }],
[ICommandService, { executeCommand }],
[IAutoFillService, { setShowMenu }],
]);
return {
accessor,
executeCommand,
setShowMenu,
};
}
describe('Sheet copy fill commands', () => {
it('copies down from the row above for a single-row selection', async () => {
const { accessor, executeCommand, setShowMenu } = createCommandAccessor({
startRow: 2,
endRow: 2,
startColumn: 1,
endColumn: 3,
});
expect(await SheetCopyDownCommand.handler(accessor)).toBe(true);
expect(executeCommand).toHaveBeenCalledWith(AutoFillCommand.id, {
sourceRange: { startRow: 1, endRow: 1, startColumn: 1, endColumn: 3 },
targetRange: { startRow: 1, endRow: 2, startColumn: 1, endColumn: 3 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
});
expect(setShowMenu).toHaveBeenCalledWith(false);
});
it('copies down from the first selected row for a multi-row selection', async () => {
const { accessor, executeCommand } = createCommandAccessor({
startRow: 1,
endRow: 4,
startColumn: 0,
endColumn: 2,
});
expect(await SheetCopyDownCommand.handler(accessor)).toBe(true);
expect(executeCommand).toHaveBeenCalledWith(AutoFillCommand.id, {
sourceRange: { startRow: 1, endRow: 1, startColumn: 0, endColumn: 2 },
targetRange: { startRow: 1, endRow: 4, startColumn: 0, endColumn: 2 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
});
});
it('copies right from the column to the left for a single-column selection', async () => {
const { accessor, executeCommand, setShowMenu } = createCommandAccessor({
startRow: 1,
endRow: 3,
startColumn: 2,
endColumn: 2,
});
expect(await SheetCopyRightCommand.handler(accessor)).toBe(true);
expect(executeCommand).toHaveBeenCalledWith(AutoFillCommand.id, {
sourceRange: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 1 },
targetRange: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
});
expect(setShowMenu).toHaveBeenCalledWith(false);
});
it('copies right from the first selected column for a multi-column selection', async () => {
const { accessor, executeCommand } = createCommandAccessor({
startRow: 2,
endRow: 4,
startColumn: 1,
endColumn: 3,
});
expect(await SheetCopyRightCommand.handler(accessor)).toBe(true);
expect(executeCommand).toHaveBeenCalledWith(AutoFillCommand.id, {
sourceRange: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 1 },
targetRange: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
});
});
it('returns false for top-row and leftmost-column no-op selections', async () => {
const topRow = createCommandAccessor({
startRow: 0,
endRow: 0,
startColumn: 1,
endColumn: 1,
});
const leftColumn = createCommandAccessor({
startRow: 1,
endRow: 1,
startColumn: 0,
endColumn: 0,
});
expect(await SheetCopyDownCommand.handler(topRow.accessor)).toBe(false);
expect(topRow.executeCommand).not.toHaveBeenCalled();
expect(topRow.setShowMenu).not.toHaveBeenCalled();
expect(await SheetCopyRightCommand.handler(leftColumn.accessor)).toBe(false);
expect(leftColumn.executeCommand).not.toHaveBeenCalled();
expect(leftColumn.setShowMenu).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,407 @@
/**
* 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, IRange, IStyleData, Nullable, Univer, Workbook } from '@univerjs/core';
import type { ISetRangeValuesCommandParams } from '../set-range-values.command';
import {
CellValueType,
ICommandService,
IConfirmService,
IUniverInstanceService,
RANGE_TYPE,
RedoCommand,
TestConfirmService,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { AddWorksheetMergeAllCommand, AddWorksheetMergeCommand } from '../add-worksheet-merge.command';
import { ClearSelectionAllCommand } from '../clear-selection-all.command';
import { ClearSelectionContentCommand } from '../clear-selection-content.command';
import { ClearSelectionFormatCommand } from '../clear-selection-format.command';
import { SetRangeValuesCommand } from '../set-range-values.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test clear selection content commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed(undefined, [
[MergeCellController],
[RefRangeService],
[IConfirmService, { useClass: TestConfirmService }],
]);
univer = testBed.univer;
get = testBed.get;
get(MergeCellController);
commandService = get(ICommandService);
commandService.registerCommand(ClearSelectionContentCommand);
commandService.registerCommand(ClearSelectionFormatCommand);
commandService.registerCommand(AddWorksheetMergeAllCommand);
commandService.registerCommand(AddWorksheetMergeCommand);
commandService.registerCommand(AddWorksheetMergeMutation);
commandService.registerCommand(RemoveWorksheetMergeMutation);
commandService.registerCommand(ClearSelectionAllCommand);
commandService.registerCommand(SetRangeValuesCommand);
commandService.registerCommand(SetRangeValuesMutation);
});
afterEach(() => {
univer.dispose();
});
describe('clear selection contents', () => {
describe('correct situations', () => {
it('will clear selection content when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getValue(): Nullable<ICellData> {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getValue();
}
expect(await commandService.executeCommand(ClearSelectionContentCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual(null);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual({
v: 'A1',
t: CellValueType.STRING,
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual(null);
// Restore the original data
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(ClearSelectionContentCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('clear selection formats', () => {
describe('correct situations', () => {
it('will clear selection format when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getValue(): Nullable<ICellData> {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getValue();
}
function getStyle(): Nullable<IStyleData> {
const value = getValue();
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
}
// set formats
const paramsStyle: ISetRangeValuesCommandParams = {
value: {
s: {
ff: 'Arial',
},
},
};
expect(await commandService.executeCommand(SetRangeValuesCommand.id, paramsStyle)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
// clear formats
expect(await commandService.executeCommand(ClearSelectionFormatCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual({
v: 'A1',
t: 1,
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual({
v: 'A1',
t: 1,
});
});
it('clear formats with merged cells', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 1, endRow: 1, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getValue(): Nullable<ICellData> {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getValue();
}
function getStyle(): Nullable<IStyleData> {
const value = getValue();
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
}
function getMerge(): IRange[] | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getConfig()
.mergeData;
}
// set formats
const paramsStyle: ISetRangeValuesCommandParams = {
value: {
s: {
ff: 'Arial',
},
},
};
// set style
expect(await commandService.executeCommand(SetRangeValuesCommand.id, paramsStyle)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
// set merge cell
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
expect(getMerge()).toStrictEqual([
{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
]);
// clear formats with merged cells
expect(await commandService.executeCommand(ClearSelectionFormatCommand.id)).toBeTruthy();
// clear formats
expect(getValue()).toStrictEqual({
v: 'A1',
t: 1,
});
// remove merge
expect(getMerge()).toStrictEqual([]);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
expect(getMerge()).toStrictEqual([
{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
]);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual({
v: 'A1',
t: 1,
});
expect(getMerge()).toStrictEqual([]);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(ClearSelectionFormatCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('clear selection all', () => {
describe('correct situations', () => {
it('will clear selection all when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getValue(): Nullable<ICellData> {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getValue();
}
function getStyle(): Nullable<IStyleData> {
const value = getValue();
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
}
// set formats
const paramsStyle: ISetRangeValuesCommandParams = {
value: {
s: {
ff: 'Arial',
},
},
};
expect(await commandService.executeCommand(SetRangeValuesCommand.id, paramsStyle)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
expect(await commandService.executeCommand(ClearSelectionAllCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual(null);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual(null);
});
it('clear all with merged cells', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 1, endRow: 1, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getValue(): Nullable<ICellData> {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getValue();
}
function getStyle(): Nullable<IStyleData> {
const value = getValue();
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
}
function getMerge(): IRange[] | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getConfig()
.mergeData;
}
// set formats
const paramsStyle: ISetRangeValuesCommandParams = {
value: {
s: {
ff: 'Arial',
},
},
};
// set style
expect(await commandService.executeCommand(SetRangeValuesCommand.id, paramsStyle)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
// set merge cell
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
expect(getMerge()).toStrictEqual([
{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
]);
// clear all with merged cells
expect(await commandService.executeCommand(ClearSelectionAllCommand.id)).toBeTruthy();
// clear formats
expect(getValue()).toStrictEqual(null);
// remove merge
expect(getMerge()).toStrictEqual([]);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getStyle()).toStrictEqual({
ff: 'Arial',
});
expect(getMerge()).toStrictEqual([
{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
]);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValue()).toStrictEqual(null);
expect(getMerge()).toStrictEqual([]);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(ClearSelectionAllCommand.id);
expect(result).toBeFalsy();
});
});
});
});

View File

@@ -0,0 +1,171 @@
/**
* 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, Workbook, Worksheet } from '@univerjs/core';
import { ICommandService, IConfigService, IUniverInstanceService, LocaleService, LocaleType, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SHEETS_PLUGIN_CONFIG_KEY } from '../../../config/config';
import enUS from '../../../locale/en-US';
import zhCN from '../../../locale/zh-CN';
import { SheetLazyExecuteScheduleService } from '../../../services/lazy-execute-schedule.service';
import { CopyWorksheetEndMutation } from '../../mutations/copy-worksheet-end.mutation';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { RemoveSheetMutation } from '../../mutations/remove-sheet.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { CopySheetCommand, getCopyUniqueSheetName } from '../copy-worksheet.command';
import { RemoveSheetCommand } from '../remove-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test copy worksheet commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed(undefined);
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(CopySheetCommand);
commandService.registerCommand(InsertSheetMutation);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(SetWorksheetActiveOperation);
commandService.registerCommand(CopyWorksheetEndMutation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(RemoveSheetCommand);
commandService.registerCommand(RemoveSheetMutation);
get(LocaleService).load({ zhCN, enUS });
});
afterEach(() => {
univer.dispose();
});
describe('copy sheet', () => {
describe('copy the only sheet', async () => {
it('correct situation', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
function getSheetCopyPart(sheet: Worksheet) {
const config = sheet.getConfig();
const { id, name, cellData, ...rest } = config;
return rest;
}
function getCellDataWithoutType(sheet: Worksheet) {
const config = sheet.getConfig();
const result: Record<number, Record<number, { v: unknown }>> = {};
for (const [rowKey, row] of Object.entries(config.cellData)) {
result[Number(rowKey)] = {};
for (const [colKey, cell] of Object.entries(row as Record<string, { v?: unknown }>)) {
result[Number(rowKey)][Number(colKey)] = { v: cell?.v };
}
}
return result;
}
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: 'sheet1' })
).toBeTruthy();
expect(
await commandService.executeCommand(CopySheetCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
})
).toBeTruthy();
const [oldSheet, newSheet] = workbook.getSheets();
expect(getSheetCopyPart(newSheet)).toEqual(getSheetCopyPart(oldSheet));
expect(getCellDataWithoutType(newSheet)).toEqual(getCellDataWithoutType(oldSheet));
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetSize()).toBe(1);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getSheetSize()).toBe(2);
const [oldSheet2, newSheet2] = workbook.getSheets();
expect(getSheetCopyPart(newSheet2)).toEqual(getSheetCopyPart(oldSheet2));
expect(getCellDataWithoutType(newSheet2)).toEqual(getCellDataWithoutType(oldSheet2));
});
it('Function getCopyUniqueSheetName', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const localeService = get(LocaleService);
localeService.setLocale(LocaleType.EN_US);
const name = 'Sheet1';
expect(getCopyUniqueSheetName(workbook, localeService, name)).toBe('Sheet1 (Copy)');
workbook.addWorksheet('sheet1-copy', 0, { name: 'Sheet1 (Copy)' });
expect(getCopyUniqueSheetName(workbook, localeService, name)).toBe('Sheet1 (Copy2)');
});
it('split large sheet copy should schedule remaining mutations and disable redo', async () => {
const sourceWorkbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const sourceSheet = sourceWorkbook.getActiveSheet();
sourceSheet.getCellMatrix().setValue(1, 0, { v: 'B1' });
expect(sourceSheet.getConfig().cellData[1]).toBeDefined();
const configService = get(IConfigService);
configService.setConfig(SHEETS_PLUGIN_CONFIG_KEY, {
largeSheetOperation: {
largeSheetCellCountThreshold: 1,
batchSize: 2,
},
});
const scheduleService = get(SheetLazyExecuteScheduleService);
const scheduleSpy = vi.spyOn(scheduleService, 'scheduleMutations');
const syncSpy = vi.spyOn(commandService, 'syncExecuteCommand');
expect(
await commandService.executeCommand(CopySheetCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
})
).toBeTruthy();
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
expect(workbook.getSheetSize()).toBe(2);
expect(scheduleSpy).toHaveBeenCalledTimes(1);
const [, copiedSheetId, scheduledMutations] = scheduleSpy.mock.calls[0] as unknown as [string, string, Array<{ id: string }>];
expect(copiedSheetId).toBeTruthy();
expect(scheduledMutations.length).toBeGreaterThan(0);
const syncOnlyCalls = syncSpy.mock.calls.filter(([, , options]) => Boolean(options?.syncOnly));
expect(syncOnlyCalls.length).toBeGreaterThan(0);
expect(syncOnlyCalls.some(([id]) => id === CopyWorksheetEndMutation.id)).toBeTruthy();
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetSize()).toBe(1);
// split mode writes empty redoMutations
expect(await commandService.executeCommand(RedoCommand.id)).toBe(false);
expect(workbook.getSheetSize()).toBe(1);
});
});
});
});

View File

@@ -0,0 +1,140 @@
/**
* 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 {
BooleanNumber,
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleService,
LocaleType,
LogLevel,
Plugin,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import enUS from '../../../locale/en-US';
import { RangeProtectionRuleModel } from '../../../models/range-protection-rule.model';
import { BorderStyleManagerService } from '../../../services/border-style-manager.service';
import { SheetLazyExecuteScheduleService } from '../../../services/lazy-execute-schedule.service';
import { WorkbookPermissionService } from '../../../services/permission/workbook-permission/workbook-permission.service';
import { WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '../../../services/permission/worksheet-permission';
import { WorksheetPermissionService } from '../../../services/permission/worksheet-permission/worksheet-permission.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SheetInterceptorService } from '../../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../../skeleton/skeleton.service';
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'A2',
},
},
},
columnData: {
1: {
hd: BooleanNumber.FALSE,
},
},
rowData: {
1: {
hd: BooleanNumber.FALSE,
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
export interface ITestBed {
univer: Univer;
get: Injector['get'];
injector: Injector;
sheet: Workbook;
}
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([WorksheetPermissionService]);
injector.add([WorksheetProtectionPointModel]);
injector.add([RangeProtectionRuleModel]);
injector.add([WorkbookPermissionService]);
injector.add([WorksheetProtectionRuleModel]);
injector.add([SheetsSelectionsService]);
injector.add([BorderStyleManagerService]);
injector.add([SheetInterceptorService]);
injector.add([SheetSkeletonService]);
injector.add([SheetLazyExecuteScheduleService]);
dependencies?.forEach((d) => injector.add(d));
this._injector.get(SheetInterceptorService);
this._injector.get(SheetSkeletonService);
this._injector.get(WorkbookPermissionService);
this._injector.get(WorksheetPermissionService);
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, Tools.deepClone(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
const localeService = injector.get(LocaleService);
localeService.load({ enUS });
return {
univer,
get: injector.get.bind(injector),
injector,
sheet,
};
}

View File

@@ -0,0 +1,340 @@
/**
* 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, IRange, IStyleData, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
import {
ICommandService,
IUniverInstanceService,
LocaleType,
RANGE_TYPE,
Rectangle,
RedoCommand,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { InsertColMutation, InsertRowMutation } from '../../mutations/insert-row-col.mutation';
import { MoveRangeMutation } from '../../mutations/move-range.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../mutations/remove-row-col.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { DeleteRangeMoveLeftCommand } from '../delete-range-move-left.command';
import { DeleteRangeMoveUpCommand } from '../delete-range-move-up.command';
import { createCommandTestBed } from './create-command-test-bed';
const mergeData = [
{
startRow: 0,
endRow: 1,
startColumn: 2,
endColumn: 2,
},
{
startRow: 1,
endRow: 2,
startColumn: 3,
endColumn: 3,
},
{
startRow: 1,
endRow: 1,
startColumn: 6,
endColumn: 7,
},
{
startRow: 1,
endRow: 1,
startColumn: 9,
endColumn: 11,
},
{
startRow: 2,
endRow: 2,
startColumn: 5,
endColumn: 6,
},
{
startRow: 2,
endRow: 2,
startColumn: 9,
endColumn: 10,
},
{
startRow: 4,
endRow: 6,
startColumn: 2,
endColumn: 2,
},
{
startRow: 5,
endRow: 6,
startColumn: 3,
endColumn: 3,
},
];
const WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'B1',
},
2: {
v: 'C1',
},
},
1: {
0: {
v: 'A2',
},
1: {
v: 'B2',
},
3: {
v: 'D2',
},
6: {
v: 'G2',
},
9: {
v: 'J2',
},
},
2: {
5: {
v: 'F3',
},
9: {
v: 'J3',
},
},
4: {
2: {
v: 'C5',
},
},
5: {
3: {
v: 'D6',
},
},
},
mergeData,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
describe('Test delete range commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let selectionManager: SheetsSelectionsService;
let getValueByPosition: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Nullable<ICellData>;
let getStyleByPosition: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Nullable<IStyleData>;
let getValues: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Array<Array<Nullable<ICellData>>> | undefined;
let getMergedCells: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => IRange[] | undefined;
beforeEach(() => {
const testBed = createCommandTestBed(WORKBOOK_DATA_DEMO);
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(DeleteRangeMoveUpCommand);
commandService.registerCommand(DeleteRangeMoveLeftCommand);
commandService.registerCommand(AddWorksheetMergeMutation);
commandService.registerCommand(RemoveWorksheetMergeMutation);
commandService.registerCommand(InsertColMutation);
commandService.registerCommand(RemoveColMutation);
commandService.registerCommand(InsertRowMutation);
commandService.registerCommand(RemoveRowMutation);
commandService.registerCommand(SetSelectionsOperation);
commandService.registerCommand(MoveRangeMutation);
commandService.registerCommand(SetRangeValuesMutation);
selectionManager = get(SheetsSelectionsService);
getValueByPosition = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): Nullable<ICellData> =>
get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(startRow, startColumn, endRow, endColumn)
.getValue();
getStyleByPosition = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): Nullable<IStyleData> => {
const value = getValueByPosition(startRow, startColumn, endRow, endColumn);
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
};
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();
getMergedCells = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): IRange[] | undefined =>
get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getMergeData()
.filter((rect) => Rectangle.intersects({ startRow, startColumn, endRow, endColumn }, rect));
});
afterEach(() => {
univer.dispose();
});
describe('Delete range move left', () => {
describe('correct situations', () => {
it('will insert range when there is a selected range, no merged cells', async () => {
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'B1',
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)!.v).toStrictEqual('A1');
expect(getValueByPosition(0, 1, 0, 1)).toStrictEqual({
v: 'B1',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'B1',
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
selectionManager.clearCurrentSelections();
const result = await commandService.executeCommand(DeleteRangeMoveLeftCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('Delete range move up', () => {
describe('correct situations', () => {
it('will insert range when there is a selected range, no merged cells', async () => {
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'A2',
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)!.v).toStrictEqual('A1');
expect(getValueByPosition(1, 0, 1, 0)).toStrictEqual({
v: 'A2',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'A2',
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
selectionManager.clearCurrentSelections();
const result = await commandService.executeCommand(DeleteRangeMoveUpCommand.id);
expect(result).toBeFalsy();
});
});
});
});

View File

@@ -0,0 +1,564 @@
/**
* 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, IRange, IStyleData, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
import {
ICommandService,
IUniverInstanceService,
LocaleType,
RANGE_TYPE,
Rectangle,
RedoCommand,
Tools,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { InsertColMutation, InsertRowMutation } from '../../mutations/insert-row-col.mutation';
import { MoveRangeMutation } from '../../mutations/move-range.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../mutations/remove-row-col.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { InsertRangeMoveDownCommand } from '../insert-range-move-down.command';
import { InsertRangeMoveRightCommand } from '../insert-range-move-right.command';
import { createCommandTestBed } from './create-command-test-bed';
const mergeData = [
{
startRow: 0,
endRow: 1,
startColumn: 2,
endColumn: 2,
},
{
startRow: 1,
endRow: 2,
startColumn: 3,
endColumn: 3,
},
{
startRow: 1,
endRow: 1,
startColumn: 6,
endColumn: 7,
},
{
startRow: 1,
endRow: 1,
startColumn: 9,
endColumn: 11,
},
{
startRow: 2,
endRow: 2,
startColumn: 5,
endColumn: 6,
},
{
startRow: 2,
endRow: 2,
startColumn: 9,
endColumn: 10,
},
{
startRow: 4,
endRow: 6,
startColumn: 2,
endColumn: 2,
},
{
startRow: 5,
endRow: 6,
startColumn: 3,
endColumn: 3,
},
];
const WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'B1',
},
2: {
v: 'C1',
},
},
1: {
0: {
v: 'A2',
},
1: {
v: 'B2',
},
3: {
v: 'D2',
},
6: {
v: 'G2',
},
9: {
v: 'J2',
},
},
2: {
5: {
v: 'F3',
},
9: {
v: 'J3',
},
},
4: {
2: {
v: 'C5',
},
},
5: {
3: {
v: 'D6',
},
},
},
mergeData,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
describe('Test insert range commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let selectionManager: SheetsSelectionsService;
let deduplicateRanges: (ranges: Array<Nullable<IRange>>) => IRange[];
let getValueByPosition: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Nullable<ICellData>;
let getStyleByPosition: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Nullable<IStyleData>;
let getValues: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => Array<Array<Nullable<ICellData>>> | undefined;
let getMergedCells: (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
) => IRange[] | undefined;
beforeEach(() => {
const data = Tools.deepClone(WORKBOOK_DATA_DEMO);
const testBed = createCommandTestBed(data, [[MergeCellController], [RefRangeService]]);
univer = testBed.univer;
get = testBed.get;
get(MergeCellController);
commandService = get(ICommandService);
commandService.registerCommand(InsertRangeMoveDownCommand);
commandService.registerCommand(InsertRangeMoveRightCommand);
commandService.registerCommand(AddWorksheetMergeMutation);
commandService.registerCommand(RemoveWorksheetMergeMutation);
commandService.registerCommand(InsertColMutation);
commandService.registerCommand(RemoveColMutation);
commandService.registerCommand(InsertRowMutation);
commandService.registerCommand(RemoveRowMutation);
commandService.registerCommand(SetSelectionsOperation);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(MoveRangeMutation);
selectionManager = get(SheetsSelectionsService);
getValueByPosition = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): Nullable<ICellData> => {
const worksheet = get(IUniverInstanceService)?.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId('sheet1');
const value = worksheet?.getRange(startRow, startColumn, endRow, endColumn);
return value?.getValue();
};
getStyleByPosition = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): Nullable<IStyleData> => {
const value = getValueByPosition(startRow, startColumn, endRow, endColumn);
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
if (value && styles) {
return styles.getStyleByCell(value);
}
};
deduplicateRanges = (ranges: Array<Nullable<IRange>>): IRange[] => {
const deduplicated: IRange[] = [];
for (const range of ranges) {
if (range) {
const exists = deduplicated.some((existingRange) => Rectangle.equals(existingRange, range));
if (!exists) {
deduplicated.push(range);
}
}
}
return deduplicated;
};
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();
getMergedCells = (
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): IRange[] | undefined =>
get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getMergeData()
.filter((rect) => Rectangle.intersects({ startRow, startColumn, endRow, endColumn }, rect));
});
afterEach(() => {
univer.dispose();
});
describe('Insert range move right', () => {
describe('correct situations', () => {
it('will insert range when there is a selected range, no merged cells', async () => {
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual(null);
expect(getValueByPosition(0, 1, 0, 1)).toStrictEqual({
v: 'A1',
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'A1',
});
expect(getValueByPosition(0, 1, 0, 1)).toStrictEqual({
v: 'B1',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual(null);
expect(getValueByPosition(0, 1, 0, 1)).toStrictEqual({
v: 'A1',
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('will insert range when there is a selected range, with merged cells of 2 rows and 1 column, break merged cells on the right', async () => {
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 2, endRow: 1, endColumn: 2, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id)).toBeTruthy();
// cell value
expect(getValueByPosition(0, 2, 0, 2)).toStrictEqual(null);
expect(getValueByPosition(0, 3, 0, 3)).toStrictEqual({
v: 'C1',
});
expect(getValueByPosition(1, 4, 1, 4)).toStrictEqual({
v: 'D2',
});
const mergedCells = deduplicateRanges(getMergedCells(0, 2, 2, 4)!);
expect(mergedCells.length).toStrictEqual(1);
// TODO@Dushusir: test undo redo after delete range completed
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('will insert range when there is a selected range, with merged cells of 3 rows and 1 column, will not break merged cells on the right', async () => {
selectionManager.setSelections([
{
range: { startRow: 4, startColumn: 2, endRow: 6, endColumn: 2, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id)).toBeTruthy();
// cell value
expect(getValueByPosition(4, 2, 4, 2)).toStrictEqual(null);
expect(getValueByPosition(4, 3, 4, 3)).toStrictEqual({
v: 'C5',
});
expect(getValueByPosition(5, 4, 5, 4)).toStrictEqual({
v: 'D6',
});
const mergedCells = deduplicateRanges(getMergedCells(4, 2, 6, 4)!);
mergedCells.forEach((mergedCell) => {
expect(
[
{
startRow: 5,
startColumn: 4,
endRow: 6,
endColumn: 4,
},
{
startRow: 4,
startColumn: 3,
endRow: 6,
endColumn: 3,
},
].some((rect) => Rectangle.intersects(rect, mergedCell))
).toStrictEqual(true);
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
selectionManager.clearCurrentSelections();
const result = await commandService.executeCommand(InsertRangeMoveRightCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('Insert range move down', () => {
describe('correct situations', () => {
it('will insert range when there is a selected range, no merged cells', async () => {
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual(null);
expect(getValueByPosition(1, 0, 1, 0)).toStrictEqual({
v: 'A1',
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual({
v: 'A1',
});
expect(getValueByPosition(1, 0, 1, 0)).toStrictEqual({
v: 'A2',
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getValueByPosition(0, 0, 0, 0)).toStrictEqual(null);
expect(getValueByPosition(1, 0, 1, 0)).toStrictEqual({
v: 'A1',
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('will insert range when there is a selected range, with merged cells of 1 row and 2 columns, break merged cells on the bottom', async () => {
selectionManager.setSelections([
{
range: { startRow: 1, startColumn: 6, endRow: 1, endColumn: 7, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id)).toBeTruthy();
// cell value
expect(getValueByPosition(1, 6, 1, 6)).toStrictEqual(null);
expect(getValueByPosition(2, 6, 2, 6)).toStrictEqual({
v: 'G2',
});
expect(getValueByPosition(2, 5, 2, 5)).toStrictEqual({
v: 'F3',
});
const mergedCells = getMergedCells(1, 5, 3, 7);
expect(deduplicateRanges(mergedCells!)).toStrictEqual([
{
startRow: 2,
startColumn: 6,
endRow: 2,
endColumn: 7,
},
]);
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('will insert range when there is a selected range, with merged cells of 1 row and 3 columns, will not break merged cells on the bottom', async () => {
selectionManager.setSelections([
{
range: { startRow: 1, startColumn: 9, endRow: 1, endColumn: 11, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id)).toBeTruthy();
// cell value
expect(getValueByPosition(1, 9, 1, 9)).toStrictEqual(null);
expect(getValueByPosition(2, 9, 2, 9)).toStrictEqual({
v: 'J2',
});
expect(getValueByPosition(3, 9, 3, 9)).toStrictEqual({
v: 'J3',
});
const mergedCells = deduplicateRanges(getMergedCells(1, 9, 3, 11)!);
expect(mergedCells.length).toStrictEqual(2);
mergedCells.forEach((mergedCell) => {
[
{
startRow: 3,
startColumn: 9,
endRow: 3,
endColumn: 10,
},
{
startRow: 2,
startColumn: 9,
endRow: 2,
endColumn: 11,
},
].some((rect) => Rectangle.equals(rect, mergedCell));
});
// reset data for next test
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('will append rows with anchor height when moving data below the current data range', async () => {
const localTestBed = createCommandTestBed({
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
1: {
0: { v: 'A2', s: 's1' },
},
},
rowData: {
0: { h: 40 },
},
rowCount: 10,
columnCount: 10,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {
s1: { bg: { rgb: '#ff0000' } },
},
}, [[MergeCellController], [RefRangeService]]);
const localGet = localTestBed.get;
localGet(MergeCellController);
const localCommandService = localGet(ICommandService);
[
InsertRangeMoveDownCommand,
AddWorksheetMergeMutation,
RemoveWorksheetMergeMutation,
InsertRowMutation,
RemoveRowMutation,
SetSelectionsOperation,
SetRangeValuesMutation,
MoveRangeMutation,
].forEach((c) => localCommandService.registerCommand(c));
await expect(localCommandService.executeCommand(InsertRangeMoveDownCommand.id, {
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 0 },
})).resolves.toBe(true);
const worksheet = localGet(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!.getSheetBySheetId('sheet1')!;
expect(worksheet.getRowCount()).toBe(13);
expect(worksheet.getRowHeight(2)).toBe(40);
expect(worksheet.getCell(4, 0)?.v).toBe('A2');
localTestBed.univer.dispose();
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
selectionManager.clearCurrentSelections();
const result = await commandService.executeCommand(InsertRangeMoveDownCommand.id);
expect(result).toBeFalsy();
});
});
});
});

View File

@@ -0,0 +1,550 @@
/**
* 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, IRange, IStyleData, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
import type { IRemoveRowColCommandParams } from '../remove-row-col.command';
import {
Direction,
ICommandService,
IUniverInstanceService,
LocaleType,
RANGE_TYPE,
RedoCommand,
Tools,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SheetInterceptorService } from '../../../services/sheet-interceptor/sheet-interceptor.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { InsertColMutation, InsertRowMutation } from '../../mutations/insert-row-col.mutation';
import { MoveRangeMutation } from '../../mutations/move-range.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../mutations/remove-row-col.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import {
InsertColAfterCommand,
InsertColBeforeCommand,
InsertColByRangeCommand,
InsertColCommand,
InsertMultiColsLeftCommand,
InsertMultiColsRightCommand,
InsertMultiRowsAboveCommand,
InsertMultiRowsAfterCommand,
InsertRowAfterCommand,
InsertRowBeforeCommand,
InsertRowByRangeCommand,
InsertRowCommand,
} from '../insert-row-col.command';
import { RemoveColByRangeCommand, RemoveColCommand, RemoveRowByRangeCommand, RemoveRowCommand } from '../remove-row-col.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test insert and remove rows cols commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createInsertRowColTestBed();
univer = testBed.univer;
get = testBed.get;
get(MergeCellController);
commandService = get(ICommandService);
[
InsertRowCommand,
InsertRowBeforeCommand,
InsertRowAfterCommand,
InsertMultiRowsAboveCommand,
InsertMultiRowsAfterCommand,
InsertColAfterCommand,
InsertColBeforeCommand,
InsertMultiColsLeftCommand,
InsertMultiColsRightCommand,
InsertColCommand,
InsertColByRangeCommand,
InsertRowByRangeCommand,
RemoveColByRangeCommand,
RemoveRowByRangeCommand,
RemoveRowCommand,
RemoveColCommand,
InsertColMutation,
InsertRowMutation,
RemoveRowMutation,
RemoveColMutation,
AddWorksheetMergeMutation,
RemoveWorksheetMergeMutation,
SetSelectionsOperation,
MoveRangeMutation,
SetRangeValuesMutation,
].forEach((c) => commandService.registerCommand(c));
});
afterEach(() => univer.dispose());
function selectRow(rowStart: number, rowEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endColumn = getColCount() - 1;
selectionManagerService.setSelections([
{
range: { startRow: rowStart, startColumn: 0, endColumn, endRow: rowEnd, rangeType: RANGE_TYPE.ROW },
primary: {
startRow: rowStart,
endRow: rowEnd,
startColumn: 0,
endColumn,
actualColumn: 0,
actualRow: rowStart,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function selectColumn(columnStart: number, columnEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endRow = getRowCount() - 1;
selectionManagerService.setSelections([
{
range: {
startRow: 0,
startColumn: columnStart,
endColumn: columnEnd,
endRow,
rangeType: RANGE_TYPE.COLUMN,
},
primary: {
startRow: 0,
endRow,
startColumn: columnStart,
endColumn: columnEnd,
actualColumn: columnStart,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function getRowCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getRowCount();
}
function getColCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColumnCount();
}
function getCellStyle(row: number, col: number): Nullable<string | IStyleData> {
return getCellInfo(row, col)?.s;
}
function getCellInfo(row: number, col: number): Nullable<ICellData> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getCellMatrix().getValue(row, col);
}
function getMergedInfo(row: number, col: number): Nullable<IRange> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getMergedCell(row, col);
}
function getMergeData() {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getMergeData();
}
describe('Insert rows', () => {
/**
* In a test case we should examine
* 1. The rows are actually inserted
* 2. Row heights are correct
* 3. Merged cells are correctly adjusted
* 4. Selections are correctly adjusted
*/
it("Should 'insert before' work", async () => {
selectRow(1, 1);
expect(getRowCount()).toBe(20);
expect(getCellStyle(2, 1)).toBe('s4');
const result = await commandService.executeCommand(InsertRowBeforeCommand.id, { value: 1 });
expect(result).toBeTruthy();
expect(getRowCount()).toBe(21);
// the merged cell should be moved down
expect(getMergedInfo(3, 2)).toEqual({ startRow: 3, endRow: 4, startColumn: 2, endColumn: 2 });
// Insert row style
expect(getCellStyle(1, 0)).toBe('s1');
await commandService.executeCommand(UndoCommand.id);
expect(getRowCount()).toBe(20);
await commandService.executeCommand(RedoCommand.id);
expect(getRowCount()).toBe(21);
});
it("Should 'insert after' work", async () => {
selectRow(1, 1);
const initRowCount = getRowCount();
expect(getRowCount()).toBe(initRowCount);
expect(getCellStyle(2, 1)).toBe('s4');
expect(getMergedInfo(2, 2)).toEqual({ startRow: 2, endRow: 3, startColumn: 2, endColumn: 2 });
const result = await commandService.executeCommand(InsertRowAfterCommand.id);
// TODO: expect row height
expect(result).toBeTruthy();
expect(getRowCount()).toBe(21);
// the merged cell should expand
expect(getMergedInfo(3, 2)).toEqual({ startRow: 3, endRow: 4, startColumn: 2, endColumn: 2 });
await commandService.executeCommand(UndoCommand.id);
expect(getRowCount()).toBe(initRowCount);
await commandService.executeCommand(RedoCommand.id);
expect(getRowCount()).toBe(21);
});
it("Should 'insert after' work, copy style", async () => {
selectRow(0, 0);
const result = await commandService.executeCommand(InsertRowAfterCommand.id);
expect(result).toBeTruthy();
// Insert row style
expect(getCellStyle(1, 0)).toBe('s1');
});
it('does not insert rows when a before-command interceptor denies the operation', async () => {
const sheetInterceptorService = get(SheetInterceptorService);
const disposable = sheetInterceptorService.interceptBeforeCommand({
performCheck: async (info) => info.id !== InsertRowCommand.id,
});
selectRow(0, 0);
await expect(commandService.executeCommand(InsertRowCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.DOWN,
range: { startRow: 1, endRow: 1, startColumn: 0, endColumn: getColCount() - 1 },
})).resolves.toBe(false);
expect(getRowCount()).toBe(20);
disposable.dispose();
});
it('copies non-default row height when inserting through the row range command', async () => {
const result = await commandService.executeCommand(InsertRowByRangeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.DOWN,
range: { startRow: 1, endRow: 2, startColumn: 0, endColumn: getColCount() - 1 },
cellValue: { 1: { 0: { s: 's1' } } },
});
expect(result).toBeTruthy();
const worksheet = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!.getSheetBySheetId('sheet1')!;
expect(worksheet.getRowHeight(1)).toBe(36);
expect(getCellStyle(1, 0)).toBe('s1');
});
it('inserts multiple rows above and below the selected planning rows', async () => {
selectRow(1, 1);
await expect(commandService.executeCommand(InsertMultiRowsAboveCommand.id, { value: 2 })).resolves.toBe(true);
expect(getRowCount()).toBe(22);
expect(getCellStyle(1, 0)).toBe('s1');
await commandService.executeCommand(UndoCommand.id);
expect(getRowCount()).toBe(20);
selectRow(1, 2);
await expect(commandService.executeCommand(InsertMultiRowsAfterCommand.id, { value: 3 })).resolves.toBe(true);
expect(getRowCount()).toBe(23);
expect(getMergedInfo(5, 2)).toMatchObject({ startRow: 2, endRow: 6, startColumn: 2, endColumn: 2 });
});
it('rejects row insertion when the requested anchor is outside the worksheet', async () => {
await expect(commandService.executeCommand(InsertRowByRangeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.DOWN,
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: getColCount() - 1 },
})).rejects.toThrow('Anchor row is out of bounds');
});
});
describe('Insert columns', () => {
it("Should 'insert before' work", async () => {
selectColumn(1, 1);
expect(getColCount()).toBe(20);
const result = await commandService.executeCommand(InsertColBeforeCommand.id, { value: 1 });
expect(result).toBeTruthy();
expect(getColCount()).toBe(21);
// Insert column style
expect(getCellStyle(0, 1)).toBe('s1');
const undoResult = await commandService.executeCommand(UndoCommand.id);
expect(undoResult).toBeTruthy();
expect(getColCount()).toBe(20);
await commandService.executeCommand(RedoCommand.id);
expect(getColCount()).toBe(21);
});
it("Should 'insert after' work", async () => {
selectColumn(1, 1);
expect(getColCount()).toBe(20);
const result = await commandService.executeCommand(InsertColAfterCommand.id);
expect(result).toBeTruthy();
expect(getColCount()).toBe(21);
// expect a merged cell to expand and a merged cell to move
expect(getMergedInfo(3, 3)).toEqual({ startRow: 2, endRow: 3, startColumn: 3, endColumn: 3 });
expect(getMergedInfo(1, 4)).toEqual({ startRow: 1, endRow: 1, startColumn: 3, endColumn: 4 });
});
it("Should 'insert after' work, copy style", async () => {
selectColumn(0, 0);
const result = await commandService.executeCommand(InsertColAfterCommand.id);
expect(result).toBeTruthy();
// Insert column style
expect(getCellStyle(0, 1)).toBe('s1');
});
it('does not insert columns when a before-command interceptor denies the operation', async () => {
const sheetInterceptorService = get(SheetInterceptorService);
const disposable = sheetInterceptorService.interceptBeforeCommand({
performCheck: async (info) => info.id !== InsertColCommand.id,
});
await expect(commandService.executeCommand(InsertColCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.RIGHT,
range: { startRow: 0, endRow: getRowCount() - 1, startColumn: 1, endColumn: 1 },
})).resolves.toBe(false);
expect(getColCount()).toBe(20);
disposable.dispose();
});
it('copies non-default column width when inserting through the column range command', async () => {
const result = await commandService.executeCommand(InsertColByRangeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.RIGHT,
range: { startRow: 0, endRow: getRowCount() - 1, startColumn: 1, endColumn: 2 },
cellValue: { 0: { 1: { s: 's1' } } },
});
expect(result).toBeTruthy();
const worksheet = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!.getSheetBySheetId('sheet1')!;
expect(worksheet.getColumnWidth(1)).toBe(112);
expect(getCellStyle(0, 1)).toBe('s1');
});
it('inserts multiple columns on both sides of a selected report block', async () => {
selectColumn(1, 1);
await expect(commandService.executeCommand(InsertMultiColsLeftCommand.id, { value: 2 })).resolves.toBe(true);
expect(getColCount()).toBe(22);
expect(getCellStyle(0, 1)).toBe('s1');
await commandService.executeCommand(UndoCommand.id);
expect(getColCount()).toBe(20);
selectColumn(1, 2);
await expect(commandService.executeCommand(InsertMultiColsRightCommand.id, { value: 3 })).resolves.toBe(true);
expect(getColCount()).toBe(23);
expect(getMergedInfo(1, 6)).toMatchObject({ startRow: 1, endRow: 1, startColumn: 2, endColumn: 6 });
});
it('rejects column insertion when the requested anchor is outside the worksheet', async () => {
await expect(commandService.executeCommand(InsertColByRangeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
direction: Direction.RIGHT,
range: { startRow: 0, endRow: getRowCount() - 1, startColumn: 0, endColumn: 0 },
})).rejects.toThrow('Anchor column is out of bounds');
});
});
describe('Remove rows', () => {
it('Should removing selected rows works', async () => {
selectRow(1, 1);
expect(getRowCount()).toBe(20);
const result = await commandService.executeCommand(RemoveRowCommand.id);
expect(result).toBeTruthy();
expect(getRowCount()).toBe(19);
expect(getMergedInfo(1, 3)).toBeFalsy(); // expect the merged cell info to be deleted
await commandService.executeCommand(UndoCommand.id);
expect(getRowCount()).toBe(20);
expect(getMergedInfo(1, 3)).toEqual({ startRow: 1, endRow: 1, startColumn: 2, endColumn: 3 }); // the merged cell should be restored
await commandService.executeCommand(RedoCommand.id);
expect(getRowCount()).toBe(19);
});
});
describe('Remove columns', () => {
it('Should removing selected rows works', async () => {
selectColumn(1, 1);
expect(getColCount()).toBe(20);
const result = await commandService.executeCommand(RemoveColCommand.id);
expect(result).toBeTruthy();
expect(getColCount()).toBe(19);
expect(getMergedInfo(1, 3)).toBeFalsy(); // expect the merged cell info to be deleted
expect(getMergedInfo(1, 2)).toEqual({ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 }); // expect the merged cell to be moved left
await commandService.executeCommand(UndoCommand.id);
expect(getColCount()).toBe(20);
await commandService.executeCommand(RedoCommand.id);
expect(getColCount()).toBe(19);
});
});
describe('Remove row where contain mergeCell', () => {
it('reduce merge cell length', async () => {
await commandService.executeCommand(RemoveRowCommand.id, {
range: {
startRow: 12,
endRow: 13,
startColumn: 1,
endColumn: 1,
},
} as IRemoveRowColCommandParams);
expect(getMergedInfo(12, 2)).toEqual({ startRow: 10, endRow: 13, startColumn: 2, endColumn: 2 });
});
});
describe('Remove col where contain mergeCell', () => {
it('reduce merge cell length', async () => {
await commandService.executeCommand(RemoveColCommand.id, {
range: {
startRow: 1,
endRow: 1,
startColumn: 12,
endColumn: 13,
},
} as IRemoveRowColCommandParams);
expect(getMergedInfo(10, 12)).toEqual({ startRow: 10, endRow: 10, startColumn: 10, endColumn: 13 });
});
});
});
const TEST_ROW_COL_INSERTION_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
s: 's1',
},
},
1: {
1: {
v: 'B2',
s: 's2',
},
4: {
v: 'E2',
s: 's3',
},
},
2: {
1: {
v: 'B3',
s: 's4',
},
},
},
mergeData: [
{ startRow: 1, endRow: 1, startColumn: 2, endColumn: 3 },
{
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 2,
},
{
startRow: 10,
endRow: 15,
startColumn: 2,
endColumn: 2,
},
{
startRow: 10,
endRow: 10,
startColumn: 10,
endColumn: 15,
},
],
rowCount: 20,
columnCount: 20,
rowData: {
0: { h: 36 },
},
columnData: {
0: { w: 112 },
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {
s1: { bg: { rgb: '#ff0000' } },
s2: { bl: 0 },
s3: { bl: 1 },
s4: { fs: 12 },
},
};
function createInsertRowColTestBed() {
return createCommandTestBed(Tools.deepClone(TEST_ROW_COL_INSERTION_DEMO), [
[MergeCellController],
[RefRangeService],
]);
}

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 { Injector, Univer, Workbook } from '@univerjs/core';
import type { LocaleKey } from '../../../locale/types';
import { ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { RemoveSheetMutation } from '../../mutations/remove-sheet.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { InsertSheetCommand } from '../insert-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test insert worksheet commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetActiveOperation);
commandService.registerCommand(RemoveSheetMutation);
});
afterEach(() => {
univer.dispose();
});
describe('insert sheet', () => {
it('should auto-generate incremental names when name is not provided', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const localeService = get(LocaleService);
const sheetPrefix = localeService.t<LocaleKey>('sheets.tabs.sheet');
expect(workbook.getSheetSize()).toBe(1);
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { rowCount: 10, columnCount: 10 },
});
expect(workbook.getSheetSize()).toBe(2);
const sheet2 = workbook.getSheets()[1];
expect(sheet2.getName()).toBe(`${sheetPrefix}1`);
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { rowCount: 10, columnCount: 10 },
});
expect(workbook.getSheetSize()).toBe(3);
const sheet3 = workbook.getSheets()[2];
expect(sheet3.getName()).toBe(`${sheetPrefix}2`);
});
it('should use provided name directly when it is unique', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { name: 'MySheet', rowCount: 10, columnCount: 10 },
});
const newSheet = workbook.getSheets()[1];
expect(newSheet.getName()).toBe('MySheet');
});
it('should deduplicate name when provided name already exists', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { name: 'sheet1', rowCount: 10, columnCount: 10 },
});
const newSheet = workbook.getSheets()[1];
expect(newSheet.getName()).not.toBe('sheet1');
});
it('should not be affected by mergeWorksheetSnapshotWithDefault default name', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const initialSize = workbook.getSheetSize();
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { rowCount: 10, columnCount: 10 },
});
const firstNewSheet = workbook.getSheets()[initialSize];
await commandService.executeCommand(InsertSheetCommand.id, {
unitId: 'test',
sheet: { rowCount: 10, columnCount: 10 },
});
const secondNewSheet = workbook.getSheets()[initialSize + 1];
expect(firstNewSheet.getName()).not.toBe(secondNewSheet.getName());
});
});
});

View File

@@ -0,0 +1,309 @@
/**
* 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, IRange, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
import {
ICommandService,
IUniverInstanceService,
LocaleType,
RedoCommand,
Tools,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { MoveRangeMutation } from '../../mutations/move-range.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { MoveRangeCommand } from '../move-range.command';
import { SetRangeValuesCommand } from '../set-range-values.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test move range commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let sheetsSelectionsService: SheetsSelectionsService;
beforeEach(() => {
const testBed = createInsertRowColTestBed();
univer = testBed.univer;
get = testBed.get;
get(MergeCellController);
commandService = get(ICommandService);
sheetsSelectionsService = get(SheetsSelectionsService);
[
AddWorksheetMergeMutation,
RemoveWorksheetMergeMutation,
SetRangeValuesCommand,
MoveRangeCommand,
MoveRangeMutation,
SetSelectionsOperation,
SetWorksheetActiveOperation,
].forEach((c) => commandService.registerCommand(c));
});
afterEach(() => univer.dispose());
describe('move range', () => {
it('move c2:d2 to g5', async () => {
const fromRange: IRange = {
startRow: 1,
endRow: 1,
startColumn: 2,
endColumn: 3,
};
const toRange: IRange = {
startRow: 1,
endRow: 1,
startColumn: 6,
endColumn: 6,
};
const result = await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange });
expect(result).toBeTruthy();
const mergeCell = getMergedInfo(1, 6);
expect(getMergeData().length).toBe(2);
expect(mergeCell).toEqual({ startRow: 1, endRow: 1, startColumn: 6, endColumn: 7 });
});
it('move a1 to c2 ', async () => {
const fromRange: IRange = {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
};
const toRange: IRange = {
startRow: 1,
endRow: 1,
startColumn: 2,
endColumn: 2,
};
const result = await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange });
expect(result).toBeFalsy();
});
it('move a1 to c2 ,should be stopped', async () => {
const fromRange: IRange = {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
};
const toRange: IRange = {
startRow: 1,
endRow: 1,
startColumn: 2,
endColumn: 2,
};
const result = await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange });
expect(result).toBeFalsy();
});
it('move merged cells requires setting the correct selection', async () => {
const fromRange: IRange = {
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 2,
};
const toRange: IRange = {
startRow: 5,
endRow: 6,
startColumn: 2,
endColumn: 2,
};
const result = await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange });
expect(result).toBeTruthy();
const selections = sheetsSelectionsService.getCurrentSelections();
expect(selections.length).toBe(1);
const selection = selections[0];
expect(selection.range).toEqual(toRange);
});
it('keeps the source merge when moving it onto an equivalent target merge', async () => {
const fromRange: IRange = {
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 2,
};
const toRange: IRange = {
startRow: 5,
endRow: 6,
startColumn: 2,
endColumn: 2,
};
await commandService.executeCommand(AddWorksheetMergeMutation.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [toRange],
});
expect(await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange })).toBeTruthy();
expect(getMergedInfo(toRange.startRow, toRange.startColumn)).toEqual(toRange);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getMergedInfo(fromRange.startRow, fromRange.startColumn)).toEqual(fromRange);
expect(getMergedInfo(toRange.startRow, toRange.startColumn)).toEqual(toRange);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getMergedInfo(fromRange.startRow, fromRange.startColumn)).toBeNull();
expect(getMergedInfo(toRange.startRow, toRange.startColumn)).toEqual(toRange);
});
it('moves merged cells across worksheets and restores merges on undo and redo', async () => {
const fromRange: IRange = {
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 2,
};
const toRange: IRange = {
startRow: 5,
endRow: 6,
startColumn: 2,
endColumn: 2,
};
await commandService.executeCommand(SetWorksheetActiveOperation.id, {
unitId: 'test',
subUnitId: 'sheet2',
});
expect(await commandService.executeCommand(MoveRangeCommand.id, {
fromRange,
toRange,
fromUnitId: 'test',
fromSubUnitId: 'sheet1',
toUnitId: 'test',
toSubUnitId: 'sheet2',
})).toBeTruthy();
expect(getMergedInfo(fromRange.startRow, fromRange.startColumn, 'sheet1')).toBeNull();
expect(getMergedInfo(toRange.startRow, toRange.startColumn, 'sheet2')).toEqual(toRange);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getMergedInfo(fromRange.startRow, fromRange.startColumn, 'sheet1')).toEqual(fromRange);
expect(getMergedInfo(toRange.startRow, toRange.startColumn, 'sheet2')).toBeNull();
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getMergedInfo(fromRange.startRow, fromRange.startColumn, 'sheet1')).toBeNull();
expect(getMergedInfo(toRange.startRow, toRange.startColumn, 'sheet2')).toEqual(toRange);
});
it('move c1:d2 to c3 ,should be replace', async () => {
const fromRange: IRange = {
startRow: 0,
endRow: 1,
startColumn: 2,
endColumn: 3,
};
const toRange: IRange = {
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 3,
};
const result = await commandService.executeCommand(MoveRangeCommand.id, { fromRange, toRange });
expect(result).toBeTruthy();
const mergeData = getMergeData();
expect(mergeData.length).toBe(1);
expect(mergeData[0]).toEqual({ startRow: 3, endRow: 3, startColumn: 2, endColumn: 3 });
});
});
function getMergedInfo(row: number, col: number, subUnitId?: string): Nullable<IRange> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = subUnitId ? workbook.getSheetBySheetId(subUnitId)! : workbook.getActiveSheet()!;
return worksheet.getMergedCell(row, col);
}
function getMergeData() {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getConfig().mergeData;
}
});
const TEST_ROW_COL_INSERTION_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
s: 's1',
},
},
1: {
1: {
v: 'B2',
s: 's2',
},
4: {
v: 'E2',
s: 's3',
},
},
2: {
1: {
v: 'B3',
s: 's4',
},
},
},
mergeData: [
{ startRow: 1, endRow: 1, startColumn: 2, endColumn: 3 },
{
startRow: 2,
endRow: 3,
startColumn: 2,
endColumn: 2,
},
],
rowCount: 20,
columnCount: 20,
},
sheet2: {
id: 'sheet2',
cellData: {},
mergeData: [],
rowCount: 20,
columnCount: 20,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: ['sheet1', 'sheet2'],
styles: {},
};
function createInsertRowColTestBed() {
return createCommandTestBed(Tools.deepClone(TEST_ROW_COL_INSERTION_DEMO), [
[MergeCellController],
[RefRangeService],
]);
}

View File

@@ -0,0 +1,436 @@
/**
* 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, IRange, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
import type { IMoveColsCommandParams, IMoveRowsCommandParams } from '../move-rows-cols.command';
import {
cellToRange,
ICommandService,
IUniverInstanceService,
LocaleType,
RANGE_TYPE,
RedoCommand,
Tools,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { MoveColsMutation, MoveRowsMutation } from '../../mutations/move-rows-cols.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { MoveColsCommand, MoveRowsCommand } from '../move-rows-cols.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test move rows cols', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createMoveRowsColsTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
[
MoveRowsCommand,
MoveColsCommand,
MoveRowsMutation,
MoveColsMutation,
RemoveWorksheetMergeMutation,
AddWorksheetMergeMutation,
SetSelectionsOperation,
].forEach((c) => commandService.registerCommand(c));
get(MergeCellController);
});
function selectRow(rowStart: number, rowEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endColumn = getColCount() - 1;
selectionManagerService.addSelections([
{
range: { startRow: rowStart, startColumn: 0, endColumn, endRow: rowEnd, rangeType: RANGE_TYPE.ROW },
primary: {
startRow: rowStart,
endRow: rowEnd,
startColumn: 0,
endColumn,
actualColumn: 0,
actualRow: rowStart,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function selectColumn(columnStart: number, columnEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endRow = getRowCount() - 1;
selectionManagerService.addSelections([
{
range: {
startRow: 0,
startColumn: columnStart,
endColumn: columnEnd,
endRow,
rangeType: RANGE_TYPE.COLUMN,
},
primary: {
startRow: 0,
endRow,
startColumn: columnStart,
endColumn: columnEnd,
actualColumn: columnStart,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function getRowCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getRowCount();
}
function getColCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColumnCount();
}
function getCellInfo(row: number, col: number): Nullable<ICellData> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getCellMatrix().getValue(row, col);
}
function getMergedInfo(row: number, col: number): Nullable<IRange> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getMergedCell(row, col);
}
function getRowHeight(row: number): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getRowHeight(row);
}
function getColWidth(col: number): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColumnWidth(col);
}
function getCurrentSelection(): IRange {
const selectionManagerService = get(SheetsSelectionsService);
const currentSelection = selectionManagerService.getCurrentSelections();
if (!currentSelection) {
throw new Error('No current selection');
}
return currentSelection[0].range;
}
describe('Move rows', () => {
it('Should move forward works', async () => {
selectRow(18, 19);
const result = await commandService.executeCommand<IMoveRowsCommandParams>(MoveRowsCommand.id, {
fromRange: { ...cellToRange(18, 1), endRow: 19 },
toRange: { ...cellToRange(1, 1), endRow: 2 },
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(3, 1)?.v).toEqual('B2');
expect(getMergedInfo(3, 1)).toEqual({ startRow: 3, endRow: 3, startColumn: 1, endColumn: 2 });
expect(getMergedInfo(4, 1)).toEqual({ startRow: 4, endRow: 5, startColumn: 1, endColumn: 1 });
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 1,
endRow: 2,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
});
const undoResult = await commandService.executeCommand(UndoCommand.id);
expect(undoResult).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(1, 1)?.v).toEqual('B2');
expect(getMergedInfo(1, 1)).toEqual({ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 });
expect(getMergedInfo(2, 1)).toEqual({ startRow: 2, endRow: 3, startColumn: 1, endColumn: 1 });
expect(getRowHeight(1)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 18,
endRow: 19,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
});
const redoResult = await commandService.executeCommand(RedoCommand.id);
expect(redoResult).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(3, 1)?.v).toEqual('B2');
expect(getMergedInfo(3, 1)).toEqual({ startRow: 3, endRow: 3, startColumn: 1, endColumn: 2 });
expect(getMergedInfo(4, 1)).toEqual({ startRow: 4, endRow: 5, startColumn: 1, endColumn: 1 });
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 1,
endRow: 2,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
});
});
it('Should move forward works with range', async () => {
selectRow(2, 2);
const result = await commandService.executeCommand<IMoveRowsCommandParams>(MoveRowsCommand.id, {
fromRange: { ...cellToRange(18, 1), endRow: 19 },
toRange: { ...cellToRange(1, 1), endRow: 2 },
range: { startRow: 18, startColumn: 0, endRow: 19, endColumn: 19, rangeType: RANGE_TYPE.ROW },
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(3, 1)?.v).toEqual('B2');
expect(getMergedInfo(3, 1)).toEqual({ startRow: 3, endRow: 3, startColumn: 1, endColumn: 2 });
expect(getMergedInfo(4, 1)).toEqual({ startRow: 4, endRow: 5, startColumn: 1, endColumn: 1 });
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 2,
endRow: 2,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
});
});
it('Should forbidding moving when parts of merged cells are selected', async () => {
selectRow(2, 2);
const result = await commandService.executeCommand<IMoveRowsCommandParams>(MoveRowsCommand.id, {
fromRange: cellToRange(2, 1),
toRange: cellToRange(1, 1),
});
expect(result).toBeFalsy();
});
it('Should forbidding moving when rows are moved across a merged cell', async () => {
selectRow(18, 19);
const result = await commandService.executeCommand<IMoveRowsCommandParams>(MoveRowsCommand.id, {
fromRange: cellToRange(18, 1),
toRange: cellToRange(3, 1),
});
expect(result).toBeFalsy();
const result2 = await commandService.executeCommand<IMoveRowsCommandParams>(MoveRowsCommand.id, {
fromRange: cellToRange(18, 1),
toRange: cellToRange(4, 1),
});
expect(result2).toBeTruthy();
});
});
describe('Move cols', () => {
it('Should move forward works', async () => {
selectColumn(18, 19);
const result = await commandService.executeCommand<IMoveColsCommandParams>(MoveColsCommand.id, {
fromRange: { ...cellToRange(1, 18), endColumn: 19 },
toRange: { ...cellToRange(1, 1), endColumn: 2 },
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(1, 3)?.v).toEqual('B2');
expect(getMergedInfo(1, 3)).toEqual({ startRow: 1, endRow: 1, startColumn: 3, endColumn: 4 });
expect(getMergedInfo(2, 3)).toEqual({ startRow: 2, endRow: 3, startColumn: 3, endColumn: 3 });
expect(getColWidth(1)).toBe(88);
expect(getColWidth(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 0,
endRow: 19,
startColumn: 1,
endColumn: 2,
rangeType: RANGE_TYPE.COLUMN,
});
const undoResult = await commandService.executeCommand(UndoCommand.id);
expect(undoResult).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(1, 1)?.v).toEqual('B2');
expect(getMergedInfo(1, 1)).toEqual({ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 });
expect(getMergedInfo(2, 1)).toEqual({ startRow: 2, endRow: 3, startColumn: 1, endColumn: 1 });
expect(getColWidth(1)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 0,
endRow: 19,
startColumn: 18,
endColumn: 19,
rangeType: RANGE_TYPE.COLUMN,
});
const redoResult = await commandService.executeCommand(RedoCommand.id);
expect(redoResult).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(1, 3)?.v).toEqual('B2');
expect(getMergedInfo(1, 3)).toEqual({ startRow: 1, endRow: 1, startColumn: 3, endColumn: 4 });
expect(getMergedInfo(2, 3)).toEqual({ startRow: 2, endRow: 3, startColumn: 3, endColumn: 3 });
expect(getColWidth(1)).toBe(88);
expect(getColWidth(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 0,
endRow: 19,
startColumn: 1,
endColumn: 2,
rangeType: RANGE_TYPE.COLUMN,
});
});
it('Should move forward works with range', async () => {
selectColumn(2, 2);
const result = await commandService.executeCommand<IMoveColsCommandParams>(MoveColsCommand.id, {
fromRange: { ...cellToRange(1, 18), endColumn: 19 },
toRange: { ...cellToRange(1, 1), endColumn: 2 },
range: { startRow: 0, startColumn: 18, endRow: 19, endColumn: 19, rangeType: RANGE_TYPE.COLUMN },
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
expect(getCellInfo(1, 3)?.v).toEqual('B2');
expect(getMergedInfo(1, 3)).toEqual({ startRow: 1, endRow: 1, startColumn: 3, endColumn: 4 });
expect(getMergedInfo(2, 3)).toEqual({ startRow: 2, endRow: 3, startColumn: 3, endColumn: 3 });
expect(getColWidth(1)).toBe(88);
expect(getColWidth(3)).toBe(30);
expect(getCurrentSelection()).toEqual({
startRow: 0,
endRow: 19,
startColumn: 2,
endColumn: 2,
rangeType: RANGE_TYPE.COLUMN,
});
});
it('Should forbidding moving when parts of merged cells are selected', async () => {
selectColumn(2, 2);
const result = await commandService.executeCommand<IMoveColsCommandParams>(MoveColsCommand.id, {
fromRange: cellToRange(1, 2),
toRange: cellToRange(1, 1),
});
expect(result).toBeFalsy();
});
it('Should forbidding moving when columns are moved across a merged cell', async () => {
selectColumn(18, 19);
const result = await commandService.executeCommand<IMoveColsCommandParams>(MoveColsCommand.id, {
fromRange: cellToRange(1, 18),
toRange: cellToRange(1, 2),
});
expect(result).toBeFalsy();
const result2 = await commandService.executeCommand<IMoveColsCommandParams>(MoveColsCommand.id, {
fromRange: cellToRange(1, 18),
toRange: cellToRange(1, 3),
});
expect(result2).toBeTruthy();
});
});
});
const TEST_ROWS_COLS_MOVE_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
s: 's1',
},
},
1: {
1: {
v: 'B2',
s: 's2',
},
4: {
v: 'E2',
s: 's3',
},
},
2: {
1: {
v: 'B3',
s: 's4',
},
},
},
mergeData: [
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 },
{
startRow: 1,
endRow: 1,
startColumn: 3,
endColumn: 4,
},
{
startRow: 2,
endRow: 3,
startColumn: 1,
endColumn: 1,
},
],
rowData: { 1: { h: 30 } },
columnData: { 1: { w: 30 } },
rowCount: 20,
columnCount: 20,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
function createMoveRowsColsTestBed() {
return createCommandTestBed(Tools.deepClone(TEST_ROWS_COLS_MOVE_DEMO), [[MergeCellController], [RefRangeService]]);
}

View File

@@ -0,0 +1,545 @@
/**
* 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, IRange } from '@univerjs/core';
import type { IRangeProtectionRule } from '../../../models/range-protection-rule.model';
import type { IWorksheetProtectionRule } from '../../../services/permission/type';
import { BooleanNumber, ICommandService, IUndoRedoService, IUniverInstanceService } from '@univerjs/core';
import { UnitObject } from '@univerjs/protocol';
import { describe, expect, it } from 'vitest';
import { EditStateEnum, RangeProtectionRuleModel, ViewStateEnum } from '../../../models/range-protection-rule.model';
import { AddRangeProtectionMutation } from '../../mutations/add-range-protection.mutation';
import { AddWorksheetProtectionMutation } from '../../mutations/add-worksheet-protection.mutation';
import { DeleteRangeProtectionMutation } from '../../mutations/delete-range-protection.mutation';
import { DeleteWorksheetProtectionMutation } from '../../mutations/delete-worksheet-protection.mutation';
import { SetRangeProtectionMutation } from '../../mutations/set-range-protection.mutation';
import { SetWorksheetColumnCountMutation } from '../../mutations/set-worksheet-column-count.mutation';
import { SetWorksheetHideMutation } from '../../mutations/set-worksheet-hide.mutation';
import { SetWorksheetPermissionPointsMutation } from '../../mutations/set-worksheet-permission-points.mutation';
import { SetWorksheetProtectionMutation } from '../../mutations/set-worksheet-protection.mutation';
import { SetWorksheetRightToLeftMutation } from '../../mutations/set-worksheet-right-to-left.mutation';
import { SetWorksheetRowCountMutation } from '../../mutations/set-worksheet-row-count.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { AddRangeProtectionCommand } from '../add-range-protection.command';
import { AddWorksheetProtectionCommand } from '../add-worksheet-protection.command';
import { DeleteRangeProtectionCommand } from '../delete-range-protection.command';
import { DeleteWorksheetProtectionCommand } from '../delete-worksheet-protection.command';
import { SetProtectionCommand } from '../set-protection.command';
import { SetWorksheetColumnCountCommand } from '../set-worksheet-column-count.command';
import { SetWorksheetPermissionPointsCommand } from '../set-worksheet-permission-points.command';
import { SetWorksheetProtectionCommand } from '../set-worksheet-protection.command';
import { SetWorksheetRightToLeftCommand } from '../set-worksheet-right-to-left.command';
import { SetWorksheetRowCountCommand } from '../set-worksheet-row-count.command';
import { SetWorksheetShowCommand } from '../set-worksheet-show.command';
function createAccessor(records: Map<unknown, unknown>): IAccessor {
return {
get: (token: unknown) => records.get(token) as never,
has: (token: unknown) => records.has(token),
} as IAccessor;
}
function createCommandService(results: Record<string, boolean> = {}) {
const calls: Array<{ id: string; params: unknown; async: boolean }> = [];
return {
calls,
executeCommand: async (id: string, params: unknown) => {
calls.push({ id, params, async: true });
return results[id] ?? true;
},
syncExecuteCommand: (id: string, params: unknown) => {
calls.push({ id, params, async: false });
return results[id] ?? true;
},
};
}
function createUndoRedoService() {
const records: unknown[] = [];
return {
records,
pushUndoRedo: (record: unknown) => records.push(record),
};
}
function createRangeRule(id: string, range: IRange = { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }): IRangeProtectionRule {
return {
id,
permissionId: `permission-${id}`,
ranges: [range],
unitId: 'unit-1',
subUnitId: 'sheet-1',
unitType: UnitObject.SelectRange,
viewState: ViewStateEnum.OthersCanView,
editState: EditStateEnum.OnlyMe,
description: 'financial input cells',
};
}
function createWorksheetRule(permissionId = 'permission-sheet-1'): IWorksheetProtectionRule {
return {
permissionId,
unitType: UnitObject.Worksheet,
unitId: 'unit-1',
subUnitId: 'sheet-1',
viewState: ViewStateEnum.OthersCanView,
editState: EditStateEnum.OnlyMe,
};
}
function createWorkbookTarget() {
const config = { hidden: BooleanNumber.TRUE, rightToLeft: BooleanNumber.FALSE };
const worksheet = {
rowCount: 100,
columnCount: 20,
config,
getSheetId: () => 'sheet-1',
getConfig: () => config,
isSheetHidden: () => config.hidden,
getRowCount() { return this.rowCount; },
getColumnCount() { return this.columnCount; },
};
const workbook = {
getUnitId: () => 'unit-1',
getSheetBySheetId: (subUnitId: string) => subUnitId === 'sheet-1' ? worksheet : null,
getActiveSheet: () => worksheet,
};
const univerInstanceService = {
getUnit: (unitId: string) => unitId === 'unit-1' ? workbook : null,
getCurrentUnitOfType: () => workbook,
};
return { worksheet, workbook, univerInstanceService };
}
describe('range protection commands', () => {
it('adds a range protection rule and records undo/redo mutations when the mutation succeeds', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const ruleModel = new RangeProtectionRuleModel();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, ruleModel],
]));
const sourceRule = createRangeRule('source-rule');
await expect(AddRangeProtectionCommand.handler(accessor, { permissionId: 'permission-new', rule: sourceRule })).resolves.toBe(true);
const addCall = commandService.calls[0];
expect(addCall.id).toBe(AddRangeProtectionMutation.id);
expect(addCall.params).toMatchObject({ unitId: 'unit-1', subUnitId: 'sheet-1' });
expect((addCall.params as { rules: IRangeProtectionRule[] }).rules[0]).toMatchObject({
ranges: sourceRule.ranges,
permissionId: 'permission-new',
description: sourceRule.description,
unitType: sourceRule.unitType,
unitId: sourceRule.unitId,
subUnitId: sourceRule.subUnitId,
viewState: sourceRule.viewState,
editState: sourceRule.editState,
});
expect(undoRedoService.records).toHaveLength(1);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
redoMutations: [{ id: AddRangeProtectionMutation.id }],
undoMutations: [{ id: DeleteRangeProtectionMutation.id }],
});
await expect(AddRangeProtectionCommand.handler(accessor, undefined as never)).resolves.toBe(false);
});
it('does not record undo when adding range protection fails', async () => {
const commandService = createCommandService({ [AddRangeProtectionMutation.id]: false });
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, new RangeProtectionRuleModel()],
]));
await expect(AddRangeProtectionCommand.handler(accessor, { permissionId: 'permission-new', rule: createRangeRule('source-rule') })).resolves.toBe(true);
expect(undoRedoService.records).toEqual([]);
});
it('deletes a range protection rule and keeps the deleted rule for undo', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
]));
const rule = createRangeRule('rule-1');
await expect(DeleteRangeProtectionCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rule })).resolves.toBe(true);
expect(commandService.calls).toEqual([{ id: DeleteRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', ruleIds: ['rule-1'] }, async: true }]);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
redoMutations: [{ id: DeleteRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', ruleIds: ['rule-1'] } }],
undoMutations: [{ id: AddRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rules: [rule] } }],
});
await expect(DeleteRangeProtectionCommand.handler(accessor, undefined as never)).resolves.toBe(false);
});
});
describe('worksheet protection commands', () => {
it('adds, deletes, and updates worksheet protection through command mutations', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
]));
const rule = createWorksheetRule('permission-new');
const oldRule = createWorksheetRule('permission-old');
await expect(AddWorksheetProtectionCommand.handler(accessor, { unitId: 'unit-1', rule })).resolves.toBe(true);
expect(commandService.calls[0]).toEqual({
id: AddWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', rule, subUnitId: 'sheet-1' },
async: true,
});
expect(DeleteWorksheetProtectionCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rule })).toBe(true);
expect(commandService.calls[1]).toEqual({
id: DeleteWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1' },
async: true,
});
await expect(SetWorksheetProtectionCommand.handler(accessor, { permissionId: 'permission-updated', rule, oldRule })).resolves.toBe(true);
expect(commandService.calls[2]).toEqual({
id: SetWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', newRule: { ...rule, permissionId: 'permission-updated' } },
async: true,
});
expect(undoRedoService.records).toHaveLength(3);
await expect(AddWorksheetProtectionCommand.handler(accessor, undefined as never)).resolves.toBe(false);
expect(DeleteWorksheetProtectionCommand.handler(accessor, undefined as never)).toBe(false);
await expect(SetWorksheetProtectionCommand.handler(accessor, undefined as never)).resolves.toBe(false);
});
it('does not record undo when worksheet protection add or set mutations fail', async () => {
const commandService = createCommandService({
[AddWorksheetProtectionMutation.id]: false,
[SetWorksheetProtectionMutation.id]: false,
});
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
]));
const rule = createWorksheetRule('permission-new');
await expect(AddWorksheetProtectionCommand.handler(accessor, { unitId: 'unit-1', rule })).resolves.toBe(true);
await expect(SetWorksheetProtectionCommand.handler(accessor, { permissionId: 'permission-updated', rule, oldRule: createWorksheetRule('permission-old') })).resolves.toBe(true);
expect(undoRedoService.records).toEqual([]);
});
it('sets worksheet point permissions with the target unit and sheet ids', async () => {
const commandService = createCommandService();
const accessor = createAccessor(new Map<unknown, unknown>([[ICommandService, commandService]]));
const rule = { unitId: 'unit-1', subUnitId: 'sheet-1', permissionId: 'permission-point-1' };
await expect(SetWorksheetPermissionPointsCommand.handler(accessor, { rule })).resolves.toBe(true);
expect(commandService.calls).toEqual([{ id: SetWorksheetPermissionPointsMutation.id, params: { rule, unitId: 'unit-1', subUnitId: 'sheet-1' }, async: true }]);
await expect(SetWorksheetPermissionPointsCommand.handler(accessor, undefined as never)).resolves.toBe(false);
});
});
describe('mixed protection command', () => {
it('updates a range protection rule in place and records the previous rule for undo', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, new RangeProtectionRuleModel()],
]));
const oldRule = createRangeRule('rule-1', { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 });
const rule = {
...oldRule,
ranges: [{ startRow: 2, endRow: 3, startColumn: 1, endColumn: 2 }],
description: 'approved input cells',
};
await expect(SetProtectionCommand.handler(accessor, { rule, oldRule } as never)).resolves.toBe(true);
expect(commandService.calls).toEqual([{
id: SetRangeProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', rule, ruleId: 'rule-1' },
async: false,
}]);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [{ id: SetRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', ruleId: 'rule-1', rule: oldRule } }],
redoMutations: [{ id: SetRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rule, ruleId: 'rule-1' } }],
});
});
it('updates a worksheet protection rule in place and records the previous rule for undo', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, new RangeProtectionRuleModel()],
]));
const oldRule = createWorksheetRule('worksheet-permission-old');
const rule = { ...oldRule, permissionId: 'worksheet-permission-new' };
await expect(SetProtectionCommand.handler(accessor, { rule, oldRule } as never)).resolves.toBe(true);
expect(commandService.calls).toEqual([{
id: SetWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', rule },
async: false,
}]);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [{ id: SetWorksheetProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rule: oldRule } }],
redoMutations: [{ id: SetWorksheetProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rule } }],
});
});
it('switches worksheet protection to range protection with reversible mutations', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const ruleModel = new RangeProtectionRuleModel();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, ruleModel],
]));
const oldRule = createWorksheetRule('worksheet-permission');
const rule = createRangeRule('', { startRow: 4, endRow: 4, startColumn: 2, endColumn: 2 });
await expect(SetProtectionCommand.handler(accessor, { rule, oldRule } as never)).resolves.toBe(true);
expect(commandService.calls[0]).toMatchObject({
id: DeleteWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1' },
});
expect(commandService.calls[1]).toMatchObject({
id: AddRangeProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', rules: [rule] },
});
expect(rule.id).not.toBe('');
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: DeleteRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', ruleIds: [rule.id] } },
{ id: AddWorksheetProtectionMutation.id, params: { unitId: 'unit-1', rule: oldRule, subUnitId: 'sheet-1' } },
],
redoMutations: [
{ id: DeleteWorksheetProtectionMutation.id },
{ id: AddRangeProtectionMutation.id },
],
});
await expect(SetProtectionCommand.handler(accessor, undefined as never)).resolves.toBe(false);
});
it('switches range protection to worksheet protection with reversible mutations', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, new RangeProtectionRuleModel()],
]));
const oldRule = createRangeRule('range-permission');
const rule = createWorksheetRule('worksheet-permission');
await expect(SetProtectionCommand.handler(accessor, { rule, oldRule } as never)).resolves.toBe(true);
expect(commandService.calls).toEqual([
{
id: DeleteRangeProtectionMutation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', ruleIds: ['range-permission'] },
async: false,
},
{
id: AddWorksheetProtectionMutation.id,
params: { unitId: 'unit-1', rule, subUnitId: 'sheet-1' },
async: false,
},
]);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: DeleteWorksheetProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1' } },
{ id: AddRangeProtectionMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rules: [oldRule] } },
],
redoMutations: [
{ id: DeleteRangeProtectionMutation.id },
{ id: AddWorksheetProtectionMutation.id },
],
});
});
it('does not create an undo item when protection update mutations fail', async () => {
const commandService = createCommandService({ [SetRangeProtectionMutation.id]: false });
const undoRedoService = createUndoRedoService();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[RangeProtectionRuleModel, new RangeProtectionRuleModel()],
]));
const oldRule = createRangeRule('rule-1');
const rule = { ...oldRule, description: 'changed description' };
await expect(SetProtectionCommand.handler(accessor, { rule, oldRule } as never)).resolves.toBe(true);
expect(undoRedoService.records).toEqual([]);
});
});
describe('worksheet row, column, rtl and show commands', () => {
it('sets row and column counts and records previous dimensions for undo', () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const { univerInstanceService } = createWorkbookTarget();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[IUniverInstanceService, univerInstanceService],
]));
expect(SetWorksheetRowCountCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rowCount: 120 })).toBe(true);
expect(SetWorksheetColumnCountCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', columnCount: 40 })).toBe(true);
expect(commandService.calls).toEqual([
{ id: SetWorksheetRowCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rowCount: 120 }, async: false },
{ id: SetWorksheetColumnCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', columnCount: 40 }, async: false },
]);
expect(undoRedoService.records).toMatchObject([
{
unitID: 'unit-1',
undoMutations: [{ id: SetWorksheetRowCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rowCount: 100 } }],
redoMutations: [{ id: SetWorksheetRowCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rowCount: 120 } }],
},
{
unitID: 'unit-1',
undoMutations: [{ id: SetWorksheetColumnCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', columnCount: 20 } }],
redoMutations: [{ id: SetWorksheetColumnCountMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', columnCount: 40 } }],
},
]);
});
it('returns false when worksheet count mutations cannot target or apply the sheet', () => {
const { univerInstanceService } = createWorkbookTarget();
const falseCommandService = createCommandService({
[SetWorksheetRowCountMutation.id]: false,
[SetWorksheetColumnCountMutation.id]: false,
});
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, falseCommandService],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, univerInstanceService],
]));
expect(SetWorksheetRowCountCommand.handler(accessor, { unitId: 'missing', subUnitId: 'sheet-1', rowCount: 120 })).toBe(false);
expect(SetWorksheetColumnCountCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'missing', columnCount: 40 })).toBe(false);
expect(SetWorksheetRowCountCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rowCount: 120 })).toBe(false);
expect(SetWorksheetColumnCountCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', columnCount: 40 })).toBe(false);
});
it('sets worksheet right-to-left mode from explicit params or defaults to false', async () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const { univerInstanceService } = createWorkbookTarget();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[IUniverInstanceService, univerInstanceService],
]));
await expect(SetWorksheetRightToLeftCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rightToLeft: BooleanNumber.TRUE })).resolves.toBe(true);
await expect(SetWorksheetRightToLeftCommand.handler(accessor)).resolves.toBe(true);
expect(commandService.calls).toEqual([
{ id: SetWorksheetRightToLeftMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rightToLeft: BooleanNumber.TRUE }, async: false },
{ id: SetWorksheetRightToLeftMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', rightToLeft: BooleanNumber.FALSE }, async: false },
]);
expect(undoRedoService.records).toHaveLength(2);
});
it('returns false when worksheet right-to-left cannot target or apply the sheet', async () => {
const { univerInstanceService } = createWorkbookTarget();
const accessorWithoutTarget = createAccessor(new Map<unknown, unknown>([
[ICommandService, createCommandService()],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, { ...univerInstanceService, getUnit: () => null, getCurrentUnitOfType: () => null }],
]));
await expect(SetWorksheetRightToLeftCommand.handler(accessorWithoutTarget, { unitId: 'missing', subUnitId: 'sheet-1', rightToLeft: BooleanNumber.TRUE })).resolves.toBe(false);
const falseCommandService = createCommandService({ [SetWorksheetRightToLeftMutation.id]: false });
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, falseCommandService],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, univerInstanceService],
]));
await expect(SetWorksheetRightToLeftCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', rightToLeft: BooleanNumber.TRUE })).resolves.toBe(false);
});
it('shows a hidden worksheet, activates it, and records undo for the hide state', () => {
const commandService = createCommandService();
const undoRedoService = createUndoRedoService();
const { univerInstanceService, worksheet } = createWorkbookTarget();
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
[IUniverInstanceService, univerInstanceService],
]));
expect(SetWorksheetShowCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(true);
expect(commandService.calls).toEqual([
{ id: SetWorksheetHideMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', hidden: BooleanNumber.FALSE }, async: false },
{ id: SetWorksheetActiveOperation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1' }, async: false },
]);
expect(undoRedoService.records[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [{ id: SetWorksheetHideMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', hidden: BooleanNumber.TRUE } }],
redoMutations: [{ id: SetWorksheetHideMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', hidden: BooleanNumber.FALSE } }],
});
worksheet.config.hidden = BooleanNumber.FALSE;
expect(SetWorksheetShowCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(false);
});
it('does not show a worksheet when targeting, hide mutation, or activation fails', () => {
const { univerInstanceService } = createWorkbookTarget();
const noTargetAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, createCommandService()],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, { ...univerInstanceService, getUnit: () => null }],
]));
expect(SetWorksheetShowCommand.handler(noTargetAccessor, { unitId: 'missing', subUnitId: 'sheet-1' })).toBe(false);
const failedHideAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, createCommandService({ [SetWorksheetHideMutation.id]: false })],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, univerInstanceService],
]));
expect(SetWorksheetShowCommand.handler(failedHideAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(false);
const failedActiveAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, createCommandService({ [SetWorksheetActiveOperation.id]: false })],
[IUndoRedoService, createUndoRedoService()],
[IUniverInstanceService, univerInstanceService],
]));
expect(SetWorksheetShowCommand.handler(failedActiveAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(false);
});
});

View File

@@ -0,0 +1,135 @@
/**
* 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 { IColorStyle, Injector, Univer, Workbook } from '@univerjs/core';
import type { RangeThemeStyle } from '../../../models/range-theme-util';
import type { ISetStyleCommandParams } from '../set-style.command';
import { ICommandService, IUniverInstanceService, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetRangeThemeModel } from '../../../models/range-theme-model';
import { SetWorksheetRangeThemeStyleMutation } from '../../mutations/add-worksheet-range-theme.mutation';
import { DeleteWorksheetRangeThemeStyleMutation } from '../../mutations/delete-worksheet-range-theme.mutation';
import { SetWorksheetRangeThemeStyleCommand } from '../add-worksheet-range-theme.command';
import { DeleteWorksheetRangeThemeStyleCommand } from '../delete-worksheet-range-theme.command';
import { SetStyleCommand } from '../set-style.command';
import { createCommandTestBed } from './create-command-test-bed';
// eslint-disable-next-line max-lines-per-function
describe('Test set worksheet default style commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let defaultTheme: RangeThemeStyle;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetWorksheetRangeThemeStyleMutation);
commandService.registerCommand(DeleteWorksheetRangeThemeStyleMutation);
commandService.registerCommand(SetWorksheetRangeThemeStyleCommand);
commandService.registerCommand(DeleteWorksheetRangeThemeStyleCommand);
commandService.registerCommand(SetStyleCommand);
const sheetRangeThemeModel = get(SheetRangeThemeModel);
defaultTheme = sheetRangeThemeModel.getDefaultRangeThemeStyle('default');
});
afterEach(() => {
univer.dispose();
});
describe('set worksheet range style', () => {
it('correct situation', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getSheetBySheetId('sheet1');
if (!workbook) throw new Error('This is an error');
expect(
await commandService.executeCommand(SetWorksheetRangeThemeStyleCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
// 'A1:D21'
range: {
startRow: 0,
startColumn: 0,
endRow: 20,
endColumn: 3,
},
themeName: 'default',
})
).toBeTruthy();
expect(worksheet?.getCell(0, 0)).toEqual(defaultTheme.getHeaderRowStyle());
expect(worksheet?.getCell(0, 1)).toEqual(defaultTheme.getHeaderRowStyle());
expect(worksheet?.getCell(1, 0)).toEqual(defaultTheme.getFirstRowStyle());
expect(worksheet?.getCell(2, 0)).toEqual(defaultTheme.getSecondRowStyle());
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
});
it('ensure range theme style can not overwrite cell style', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
await commandService.executeCommand(SetStyleCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: {
startRow: 0,
startColumn: 0,
endRow: 1,
endColumn: 1,
},
style: {
type: 'bg',
value: {
rgb: 'red',
},
},
} as ISetStyleCommandParams<IColorStyle>);
expect(
await commandService.executeCommand(SetWorksheetRangeThemeStyleCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
// 'A1:D21'
range: {
startRow: 0,
startColumn: 0,
endRow: 20,
endColumn: 3,
},
themeName: 'default',
})
).toBeTruthy();
const worksheet = workbook.getSheetBySheetId('sheet1');
expect(worksheet?.getCell(0, 0)).toEqual({
bg: {
rgb: 'red',
},
});
expect(worksheet?.getCell(0, 1)).toEqual(defaultTheme.getHeaderRowStyle());
});
});
});

View File

@@ -0,0 +1,207 @@
/**
* 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 { IRemoveRowColCommandParams } from '../remove-row-col.command';
import {
ICommandService,
IUniverInstanceService,
LocaleType,
RANGE_TYPE,
RedoCommand,
Tools,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { beforeEach, describe, expect, it } from 'vitest';
import { MergeCellController } from '../../../controllers/merge-cell.controller';
import { RefRangeService } from '../../../services/ref-range/ref-range.service';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { InsertColMutation, InsertRowMutation } from '../../mutations/insert-row-col.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../mutations/remove-row-col.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { InsertColByRangeCommand, InsertRowByRangeCommand } from '../insert-row-col.command';
import { RemoveColByRangeCommand, RemoveColCommand, RemoveRowByRangeCommand, RemoveRowCommand } from '../remove-row-col.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test remove rows cols', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createMoveRowsColsTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
[
RemoveRowCommand,
RemoveColCommand,
RemoveRowByRangeCommand,
RemoveColByRangeCommand,
RemoveColMutation,
RemoveRowMutation,
InsertRowMutation,
InsertColMutation,
InsertRowByRangeCommand,
InsertColByRangeCommand,
SetSelectionsOperation,
SetRangeValuesMutation,
].forEach((c) => commandService.registerCommand(c));
get(MergeCellController);
});
function selectRow(rowStart: number, rowEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endColumn = getColCount() - 1;
selectionManagerService.addSelections([
{
range: { startRow: rowStart, startColumn: 0, endColumn, endRow: rowEnd, rangeType: RANGE_TYPE.ROW },
primary: {
startRow: rowStart,
endRow: rowEnd,
startColumn: 0,
endColumn,
actualColumn: 0,
actualRow: rowStart,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function getColCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColumnCount();
}
function getCellInfo(row: number, col: number): Nullable<ICellData> {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getCellMatrix().getValue(row, col);
}
describe('Remove rows', () => {
it('Should move to up works, undo will recover the deleted data', async () => {
selectRow(0, 0);
const result = await commandService.executeCommand<IRemoveRowColCommandParams>(RemoveRowCommand.id, {
range: {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 19,
rangeType: RANGE_TYPE.ROW,
},
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('B1');
// undo
await commandService.executeCommand(UndoCommand.id);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
// redo
await commandService.executeCommand(RedoCommand.id);
expect(getCellInfo(0, 0)?.v).toEqual('B1');
});
});
describe('Remove cols', () => {
it('Should move to left works, undo will recover the deleted data', async () => {
selectRow(0, 0);
const result = await commandService.executeCommand<IRemoveRowColCommandParams>(RemoveColCommand.id, {
range: {
startRow: 0,
endRow: 19,
startColumn: 0,
endColumn: 0,
rangeType: RANGE_TYPE.COLUMN,
},
});
expect(result).toEqual(true);
expect(getCellInfo(0, 0)?.v).toEqual('A2');
// undo
await commandService.executeCommand(UndoCommand.id);
expect(getCellInfo(0, 0)?.v).toEqual('A1');
// redo
await commandService.executeCommand(RedoCommand.id);
expect(getCellInfo(0, 0)?.v).toEqual('A2');
});
});
});
const TEST_ROWS_COLS_MOVE_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
s: 's1',
},
1: {
v: 'A2',
s: 's1',
},
},
1: {
0: {
v: 'B1',
s: 's1',
},
1: {
v: 'B2',
s: 's2',
},
4: {
v: 'E2',
s: 's3',
},
},
2: {
1: {
v: 'B3',
s: 's4',
},
},
},
rowData: { 1: { h: 30 } },
columnData: { 1: { w: 30 } },
rowCount: 20,
columnCount: 20,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
function createMoveRowsColsTestBed() {
return createCommandTestBed(Tools.deepClone(TEST_ROWS_COLS_MOVE_DEMO), [[MergeCellController], [RefRangeService]]);
}

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 { Injector, Univer, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { RemoveSheetMutation } from '../../mutations/remove-sheet.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { InsertSheetCommand } from '../insert-sheet.command';
import { RemoveSheetCommand } from '../remove-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test remove worksheet commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(RemoveSheetCommand);
commandService.registerCommand(RemoveSheetMutation);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetActiveOperation);
});
afterEach(() => {
univer.dispose();
});
describe('remove sheet', () => {
describe('remove the only sheet', async () => {
it('will be not working if there is only one sheet', async () => {
expect(
await commandService.executeCommand(RemoveSheetCommand.id, {
unitId: 'test',
sheetId: 'sheet1',
})
).toBeFalsy();
});
it('will be working if there are more than one sheet', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const targetActiveSheet = workbook.getActiveSheet();
const targetSheetId = targetActiveSheet?.getSheetId();
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(
await commandService.executeCommand(RemoveSheetCommand.id, {
unitId: 'test',
sheetId: 'sheet1',
})
).toBeTruthy();
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetSize()).toBe(2);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getSheetSize()).toBe(1);
});
});
});
});

View File

@@ -0,0 +1,137 @@
/**
* 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, IWorkbookData, Univer, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, LocaleType, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { RemoveWorksheetMergeCommand } from '../remove-worksheet-merge.command';
import { createCommandTestBed } from './create-command-test-bed';
const TEST_WORKBOOK: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: ['sheet1'],
styles: {
s1: {
ff: 'Arial',
},
},
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
rowCount: 20,
columnCount: 20,
mergeData: [
{
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
],
cellData: {
0: {
0: { v: 'A1', s: 's1' },
1: {},
},
1: {
0: {},
1: {},
},
},
rowData: {},
columnData: {},
},
},
};
describe('remove-worksheet-merge.command', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed(TEST_WORKBOOK);
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
[
RemoveWorksheetMergeCommand,
AddWorksheetMergeMutation,
RemoveWorksheetMergeMutation,
SetRangeValuesMutation,
SetSelectionsOperation,
].forEach((command) => commandService.registerCommand(command));
});
afterEach(() => {
univer.dispose();
});
function getActiveWorksheet() {
return get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
}
it('returns false for empty ranges or no merge intersection', async () => {
expect(
await commandService.executeCommand(RemoveWorksheetMergeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [],
})
).toBe(false);
expect(
await commandService.executeCommand(RemoveWorksheetMergeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [{ startRow: 10, endRow: 11, startColumn: 10, endColumn: 11 }],
})
).toBe(false);
});
it('removes merge and keeps main-cell style after unmerge', async () => {
const worksheet = getActiveWorksheet();
expect(worksheet.getMergeData()).toHaveLength(1);
expect(
await commandService.executeCommand(RemoveWorksheetMergeCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }],
})
).toBe(true);
expect(worksheet.getMergeData()).toHaveLength(0);
expect(worksheet.getCell(0, 1)?.s).toBe('s1');
expect(worksheet.getCell(1, 0)?.s).toBe('s1');
expect(worksheet.getCell(1, 1)?.s).toBe('s1');
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(worksheet.getMergeData()).toHaveLength(1);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(worksheet.getMergeData()).toHaveLength(0);
});
});

View File

@@ -0,0 +1,382 @@
/**
* 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 { IBorderData, Injector, IRange, Univer, Workbook } from '@univerjs/core';
import { BorderStyleTypes, BorderType, ICommandService, IConfirmService, IUniverInstanceService, RANGE_TYPE, TestConfirmService, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../../mutations/add-worksheet-merge.mutation';
import { RemoveWorksheetMergeMutation } from '../../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { AddWorksheetMergeAllCommand, AddWorksheetMergeCommand } from '../add-worksheet-merge.command';
import { RemoveWorksheetMergeCommand } from '../remove-worksheet-merge.command';
import {
SetBorderBasicCommand,
SetBorderColorCommand,
SetBorderCommand,
SetBorderPositionCommand,
SetBorderStyleCommand,
} from '../set-border.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test style commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let getBorder: ({ startRow, startColumn }: IRange) => IBorderData | undefined;
beforeEach(() => {
const testBed = createCommandTestBed(undefined, [
[IConfirmService, { useClass: TestConfirmService }],
]);
univer = testBed.univer;
get = testBed.get;
getBorder = ({ startRow, startColumn }: IRange): IBorderData | undefined => {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(startRow, startColumn)
.getBorder();
};
commandService = get(ICommandService);
[
SetBorderBasicCommand,
SetBorderColorCommand,
SetBorderStyleCommand,
SetBorderCommand,
SetBorderPositionCommand,
AddWorksheetMergeCommand,
AddWorksheetMergeAllCommand,
RemoveWorksheetMergeCommand,
AddWorksheetMergeMutation,
RemoveWorksheetMergeMutation,
SetRangeValuesMutation,
].forEach((c) => commandService.registerCommand(c));
});
afterEach(() => univer.dispose());
describe('set border style', () => {
describe('correct situations', () => {
it('will set border style when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.TOP })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#abcdef' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.DOTTED })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.t).toStrictEqual({
s: BorderStyleTypes.DOTTED,
cl: { rgb: '#abcdef' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.LEFT })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#123456' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.DASHED })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 })?.l).toStrictEqual({
s: BorderStyleTypes.DASHED,
cl: { rgb: '#123456' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.BOTTOM })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#654321' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.DASH_DOT })
).toBeTruthy();
expect(getBorder({ startRow: 5, endRow: 5, startColumn: 0, endColumn: 0 })?.b).toStrictEqual({
s: BorderStyleTypes.DASH_DOT,
cl: { rgb: '#654321' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.RIGHT })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#666666' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.DOUBLE })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 5, endColumn: 5 })?.r).toStrictEqual({
s: BorderStyleTypes.DOUBLE,
cl: { rgb: '#666666' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.VERTICAL })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#777777' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.HAIR })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.r).toStrictEqual({
s: BorderStyleTypes.HAIR,
cl: { rgb: '#777777' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.HORIZONTAL })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#111111' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.MEDIUM })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toStrictEqual({
s: BorderStyleTypes.MEDIUM,
cl: { rgb: '#111111' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.NONE })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#111111' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, { value: BorderStyleTypes.MEDIUM })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toBeNull();
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.OUTSIDE })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#333333' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.MEDIUM_DASHED,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.l).toStrictEqual({
s: BorderStyleTypes.MEDIUM_DASHED,
cl: { rgb: '#333333' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.INSIDE })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#888888' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.MEDIUM_DASH_DOT_DOT,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toStrictEqual({
s: BorderStyleTypes.MEDIUM_DASH_DOT_DOT,
cl: { rgb: '#888888' },
});
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.NONE })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#888888' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.MEDIUM_DASH_DOT_DOT,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toBeNull();
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.ALL })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#aaaaaa' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.SLANT_DASH_DOT,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toStrictEqual({
s: BorderStyleTypes.SLANT_DASH_DOT,
cl: { rgb: '#aaaaaa' },
});
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.ALL })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#aaaaaa' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.SLANT_DASH_DOT,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.b).toStrictEqual({
s: BorderStyleTypes.SLANT_DASH_DOT,
cl: { rgb: '#aaaaaa' },
});
expect(
await commandService.executeCommand(SetBorderBasicCommand.id, {
value: {
type: BorderType.TOP,
color: '#123456',
style: BorderStyleTypes.DASHED,
},
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })?.t).toStrictEqual({
s: BorderStyleTypes.DASHED,
cl: { rgb: '#123456' },
});
});
it('set border for range has merged and unmerged cells', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.ALL })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderColorCommand.id, { value: '#aaaaaa' })
).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderStyleCommand.id, {
value: BorderStyleTypes.SLANT_DASH_DOT,
})
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 5, endColumn: 5 })?.r).toStrictEqual({
s: BorderStyleTypes.SLANT_DASH_DOT,
cl: { rgb: '#aaaaaa' },
});
selectionManager.clear();
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.NONE });
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 5, endColumn: 5 })?.r).toBeNull();
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endRow: 5, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
// executeCommand('sheet.command.set-border-position', { value: 'inside' })
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.INSIDE });
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 5, endColumn: 5 })?.r).toBeFalsy();
expect(getBorder({ startRow: 1, endRow: 1, startColumn: 5, endColumn: 5 })?.r).toBeFalsy();
expect(getBorder({ startRow: 1, endRow: 1, startColumn: 5, endColumn: 5 })?.b).not.toBeNull();
expect(getBorder({ startRow: 1, endRow: 1, startColumn: 5, endColumn: 5 })?.t).not.toBeNull();
expect(getBorder({ startRow: 1, endRow: 1, startColumn: 5, endColumn: 5 })?.l).not.toBeNull();
});
it('set border and clear border in adjacent cells', async () => {
// merge B1:B3, and set BorderType.ALL
// then set BorderType.NONE for A2
// The left border of B1 B3 should be kept, only left border of B2 should be none;
const selectionManager = get(SheetsSelectionsService);
selectionManager.setSelections([
{
range: { startRow: 0, startColumn: 1, endRow: 2, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.ALL })
).toBeTruthy();
selectionManager.setSelections([
{
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
expect(
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.NONE })
).toBeTruthy();
expect(getBorder({ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 })?.l).toStrictEqual({
s: BorderStyleTypes.THIN,
cl: { rgb: '#000000' },
});
expect(getBorder({ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 })?.l).toBeFalsy();
expect(getBorder({ startRow: 2, endRow: 2, startColumn: 1, endColumn: 1 })?.l).toStrictEqual({
s: BorderStyleTypes.THIN,
cl: { rgb: '#000000' },
});
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetBorderPositionCommand.id, {
value: BorderType.TOP,
});
expect(result).toBeFalsy();
});
});
});
});

View File

@@ -0,0 +1,158 @@
/**
* 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, IWorkbookData, Univer, Workbook } from '@univerjs/core';
import type { ISetColDataCommandParams } from '../set-col-data.command';
import { BooleanNumber, ICommandService, IUniverInstanceService, LocaleType, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetColDataMutation } from '../../mutations/set-col-data.mutation';
import { SetColDataCommand } from '../set-col-data.command';
import { createCommandTestBed } from './create-command-test-bed';
const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => {
return {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'A2',
},
},
},
columnData: {
1: {
hd: BooleanNumber.FALSE,
},
5: {
custom: {
key: 'value',
},
},
},
rowData: {
1: {
hd: BooleanNumber.FALSE,
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
};
describe('test set column data commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
function getColumnData(column: number) {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const columnManager = worksheet.getColumnManager();
return columnManager.getColumn(column);
}
beforeEach(() => {
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA_DEMO());
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetColDataCommand);
commandService.registerCommand(SetColDataMutation);
});
afterEach(() => univer.dispose());
it('should set custom properties on columns', async () => {
expect(getColumnData(1)).toEqual({ hd: BooleanNumber.FALSE });
const params: ISetColDataCommandParams = {
unitId: 'test',
subUnitId: 'sheet1',
columnData: {
1: {
custom: {
color: 'red',
},
},
2: {
custom: {
color: 'green',
},
},
5: {
custom: undefined,
},
},
};
await commandService.executeCommand(SetColDataCommand.id, params);
expect(getColumnData(1)).toEqual({
hd: BooleanNumber.FALSE,
custom: {
color: 'red',
},
});
expect(getColumnData(2)).toEqual({
custom: {
color: 'green',
},
});
expect(getColumnData(5)).toEqual({
custom: undefined,
});
await commandService.executeCommand(UndoCommand.id);
expect(getColumnData(1)).toEqual({
hd: BooleanNumber.FALSE,
});
expect(getColumnData(2)).toBeUndefined();
expect(getColumnData(5)).toEqual({
custom: {
key: 'value',
},
});
await commandService.executeCommand(RedoCommand.id);
expect(getColumnData(1)).toEqual({
hd: BooleanNumber.FALSE,
custom: {
color: 'red',
},
});
expect(getColumnData(2)).toEqual({
custom: {
color: 'green',
},
});
expect(getColumnData(5)).toEqual({
custom: undefined,
});
});
});

View File

@@ -0,0 +1,223 @@
/**
* 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, Workbook } from '@univerjs/core';
import type { IDeltaColumnWidthCommandParams, ISetColWidthCommandParams } from '../set-worksheet-col-width.command';
import { ICommandService, IUniverInstanceService, RANGE_TYPE, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SetWorksheetColWidthMutation } from '../../mutations/set-worksheet-col-width.mutation';
import { DeltaColumnWidthCommand, SetColWidthCommand, SetWorksheetColIsAutoWidthCommand } from '../set-worksheet-col-width.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set col width commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let selectionsService: SheetsSelectionsService;
function getColumnWidth(col: number): number {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
return worksheet.getColumnWidth(col);
}
function setSelection(start: number, end: number): void {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const maxRow = worksheet.getMaxRows() - 1;
selectionsService.setSelections([
{
range: { startRow: 0, startColumn: start, endColumn: end, endRow: maxRow, rangeType: RANGE_TYPE.COLUMN },
primary: {
startRow: 0,
startColumn: start,
endColumn: start,
endRow: 0,
actualColumn: start,
actualRow: 1,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function addSelection(start: number, end: number): void {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const maxRow = worksheet.getMaxRows() - 1;
selectionsService.addSelections([
{
range: { startRow: 0, startColumn: start, endColumn: end, endRow: maxRow, rangeType: RANGE_TYPE.COLUMN },
primary: null,
style: null,
},
]);
}
function selectWholeSheet(): void {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
selectionsService.setSelections([
{
range: {
startRow: 0,
endRow: worksheet.getMaxRows() - 1,
startColumn: 0,
endColumn: worksheet.getMaxColumns() - 1,
rangeType: RANGE_TYPE.ALL,
},
primary: null,
style: null,
},
]);
}
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(DeltaColumnWidthCommand);
commandService.registerCommand(SetColWidthCommand);
commandService.registerCommand(SetWorksheetColIsAutoWidthCommand);
commandService.registerCommand(SetWorksheetColWidthMutation);
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const maxRow = worksheet.getMaxRows() - 1;
selectionsService = get(SheetsSelectionsService);
// Select col 1 and 5
setSelection(1, 2);
addSelection(5, 5);
});
afterEach(() => univer.dispose());
describe('Delta col widths by dragging', () => {
it('Should expand all col selections when anchor col is selected', async () => {
expect(getColumnWidth(1)).toBe(88);
await commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, {
deltaX: -23,
anchorCol: 5,
});
expect(getColumnWidth(1)).toBe(65);
expect(getColumnWidth(2)).toBe(65);
expect(getColumnWidth(5)).toBe(65);
await commandService.executeCommand(UndoCommand.id);
expect(getColumnWidth(1)).toBe(88);
expect(getColumnWidth(2)).toBe(88);
expect(getColumnWidth(5)).toBe(88);
await commandService.executeCommand(RedoCommand.id);
expect(getColumnWidth(1)).toBe(65);
expect(getColumnWidth(2)).toBe(65);
expect(getColumnWidth(5)).toBe(65);
});
// Fix https://github.com/dream-num/univer-pro/issues/2302.
it('Should undo to original col widths', async () => {
expect(getColumnWidth(1)).toBe(88);
setSelection(1, 1);
await commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, { deltaX: -23, anchorCol: 1 });
expect(getColumnWidth(1)).toBe(65);
expect(getColumnWidth(2)).toBe(88);
setSelection(2, 2);
await commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, { deltaX: 22, anchorCol: 2 });
expect(getColumnWidth(1)).toBe(65);
expect(getColumnWidth(2)).toBe(110);
setSelection(1, 2);
await commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, { deltaX: 50, anchorCol: 2 });
expect(getColumnWidth(1)).toBe(160);
expect(getColumnWidth(2)).toBe(160);
await commandService.executeCommand(UndoCommand.id);
expect(getColumnWidth(1)).toBe(65);
expect(getColumnWidth(2)).toBe(110);
});
it('Should expand only the anchor col in other situations', () => {
expect(getColumnWidth(1)).toBe(88);
expect(getColumnWidth(7)).toBe(88);
commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, {
deltaX: -23,
anchorCol: 7,
});
expect(getColumnWidth(1)).toBe(88);
expect(getColumnWidth(2)).toBe(88);
expect(getColumnWidth(5)).toBe(88);
expect(getColumnWidth(7)).toBe(65);
});
it('resizes every column when the whole sheet is selected', async () => {
selectWholeSheet();
await commandService.executeCommand<IDeltaColumnWidthCommandParams>(DeltaColumnWidthCommand.id, {
deltaX: 12,
anchorCol: 0,
});
expect(getColumnWidth(0)).toBe(100);
expect(getColumnWidth(3)).toBe(100);
await commandService.executeCommand(UndoCommand.id);
expect(getColumnWidth(0)).toBe(88);
expect(getColumnWidth(3)).toBe(88);
});
});
it('Direct change col widths', async () => {
expect(getColumnWidth(1)).toBe(88);
await commandService.executeCommand<ISetColWidthCommandParams>(SetColWidthCommand.id, {
value: 40,
});
expect(getColumnWidth(1)).toBe(40);
expect(getColumnWidth(2)).toBe(40);
expect(getColumnWidth(5)).toBe(40);
await commandService.executeCommand(UndoCommand.id);
expect(getColumnWidth(1)).toBe(88);
expect(getColumnWidth(2)).toBe(88);
expect(getColumnWidth(5)).toBe(88);
await commandService.executeCommand(RedoCommand.id);
expect(getColumnWidth(1)).toBe(40);
expect(getColumnWidth(2)).toBe(40);
expect(getColumnWidth(5)).toBe(40);
});
it('Direct change col widths with ranges', async () => {
await commandService.executeCommand<ISetColWidthCommandParams>(SetColWidthCommand.id, {
value: 50,
ranges: [{ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }],
});
expect(getColumnWidth(1)).toBe(50);
});
it('records auto-width work for the selected columns', async () => {
expect(await commandService.executeCommand(SetWorksheetColIsAutoWidthCommand.id)).toBe(true);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
});
});

View File

@@ -0,0 +1,177 @@
/**
* 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, IWorkbookData, Univer, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, LocaleType, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetFrozenMutation } from '../../mutations/set-frozen.mutation';
import { SetFrozenCommand } from '../set-frozen.command';
import { createCommandTestBed } from './create-command-test-bed';
const WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'B1',
},
2: {
v: 'C1',
},
},
1: {
0: {
v: 'A2',
},
1: {
v: 'B2',
},
3: {
v: 'D2',
},
6: {
v: 'G2',
},
9: {
v: 'J2',
},
},
2: {
5: {
v: 'F3',
},
9: {
v: 'J3',
},
},
4: {
2: {
v: 'C5',
},
},
5: {
3: {
v: 'D6',
},
},
},
mergeData: [],
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
describe('Test set frozen commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed(WORKBOOK_DATA_DEMO);
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetFrozenCommand);
commandService.registerCommand(SetFrozenMutation);
});
afterEach(() => {
univer.dispose();
});
describe('set frozen', () => {
describe('set frozen', async () => {
it('correct situation: ', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const targetActiveSheet = workbook.getActiveSheet()!;
const targetSheetId = targetActiveSheet?.getSheetId();
const originFreeze = workbook.getSheetBySheetId(targetSheetId)?.getConfig().freeze;
expect(
await commandService.executeCommand(SetFrozenCommand.id, {
startRow: 1,
startColumn: 1,
xSplit: 1,
ySplit: 1,
})
).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().freeze).toEqual({
startRow: 1,
startColumn: 1,
xSplit: 1,
ySplit: 1,
});
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().freeze).toEqual({
startRow: 1,
startColumn: 1,
xSplit: 1,
ySplit: 1,
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().freeze).toEqual(originFreeze);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().freeze).toEqual({
startRow: 1,
startColumn: 1,
xSplit: 1,
ySplit: 1,
});
});
it('wrong situation: ', async () => {
expect(
await commandService.executeCommand(SetFrozenCommand.id, {
startRow: 100,
startColumn: 100,
xSplit: 1,
ySplit: 1,
})
).toBeFalsy();
});
it('should reject incomplete freeze params without changing the worksheet', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
const originFreeze = worksheet.getFreeze();
const params = {
startRow: 1,
startColumn: 0,
xSplit: 0,
};
expect(await commandService.executeCommand(SetFrozenCommand.id, params)).toBeFalsy();
expect(worksheet.getFreeze()).toEqual(originFreeze);
});
});
});
});

View File

@@ -0,0 +1,95 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetGridlinesColorMutation } from '../../mutations/set-gridlines-color.mutation';
import { SetGridlinesColorCommand } from '../set-gridlines-color.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet default style commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetGridlinesColorCommand);
commandService.registerCommand(SetGridlinesColorMutation);
});
afterEach(() => {
univer.dispose();
});
describe('set worksheet grid line color', () => {
it('correct situation', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getSheetBySheetId('sheet1');
if (!workbook) throw new Error('This is an error');
const color = '#ff0000';
expect(
await commandService.executeCommand(SetGridlinesColorCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
color,
})
).toBeTruthy();
expect(worksheet?.getGridlinesColor()).toEqual(color);
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(worksheet?.getGridlinesColor()).toBeUndefined();
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(worksheet?.getGridlinesColor()).toEqual(color);
});
});
it('reset worksheet grid line color', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getSheetBySheetId('sheet1');
if (!workbook) throw new Error('This is an error');
const color = '#ff0000';
expect(
await commandService.executeCommand(SetGridlinesColorCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
color,
})
).toBeTruthy();
expect(worksheet?.getGridlinesColor()).toEqual(color);
expect(
await commandService.executeCommand(SetGridlinesColorCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
color: undefined,
})
).toBeTruthy();
expect(worksheet?.getGridlinesColor()).toBeUndefined();
});
});

View File

@@ -0,0 +1,103 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IWorkbookData, Univer, Workbook } from '@univerjs/core';
import { BorderStyleTypes, CellValueType, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetRangeCustomMetadataCommand } from '../set-range-custom-metadata.command';
import { createCommandTestBed } from './create-command-test-bed';
const WORKBOOK_DATA: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: ['sheet1'],
styles: {},
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
2: {
s: {
bd: {
t: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } },
r: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } },
b: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } },
l: { s: BorderStyleTypes.THIN, cl: { rgb: '#000000' } },
},
},
t: CellValueType.STRING,
},
},
},
},
},
};
describe('SetRangeCustomMetadataCommand', () => {
let univer: Univer;
let commandService: ICommandService;
let univerInstanceService: IUniverInstanceService;
beforeEach(() => {
const testBed = createCommandTestBed(WORKBOOK_DATA);
univer = testBed.univer;
commandService = testBed.get(ICommandService);
univerInstanceService = testBed.get(IUniverInstanceService);
commandService.registerCommand(SetRangeCustomMetadataCommand);
commandService.registerCommand(SetRangeValuesMutation);
});
afterEach(() => univer.dispose());
it('preserves existing cell type and border style when setting metadata on a styled empty cell', async () => {
const result = await commandService.executeCommand(SetRangeCustomMetadataCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
range: {
startRow: 0,
endRow: 0,
startColumn: 2,
endColumn: 2,
rangeType: RANGE_TYPE.NORMAL,
},
customMetadata: {
custom: { key: 'value' },
},
});
const workbook = univerInstanceService.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!;
const cell = workbook.getSheetBySheetId('sheet1')!.getCellRaw(0, 2);
const style = workbook.getStyles().getStyleByCell(cell);
expect(result).toBe(true);
expect(cell?.custom).toEqual({ key: 'value' });
expect(cell?.t).toBe(CellValueType.STRING);
expect(style).toMatchObject({
bd: {
t: { s: BorderStyleTypes.THIN },
r: { s: BorderStyleTypes.THIN },
b: { s: BorderStyleTypes.THIN },
l: { s: BorderStyleTypes.THIN },
},
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RANGE_TYPE, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SetColHiddenMutation, SetColVisibleMutation } from '../../mutations/set-col-visible.mutation';
import { SetRowHiddenMutation, SetRowVisibleMutation } from '../../mutations/set-row-visible.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import {
SetColHiddenCommand,
SetSelectedColsVisibleCommand,
SetSpecificColsVisibleCommand,
} from '../set-col-visible.command';
import {
SetRowHiddenCommand,
SetSelectedRowsVisibleCommand,
SetSpecificRowsVisibleCommand,
} from '../set-row-visible.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test row col hide/unhine commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
[
SetRowHiddenCommand,
SetRowHiddenMutation,
SetColHiddenCommand,
SetColHiddenMutation,
SetSelectedRowsVisibleCommand,
SetSelectedColsVisibleCommand,
SetRowVisibleMutation,
SetColVisibleMutation,
SetSpecificColsVisibleCommand,
SetSpecificRowsVisibleCommand,
SetSelectionsOperation,
].forEach((command) => {
commandService.registerCommand(command);
});
});
afterEach(() => univer.dispose());
function getRowCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getRowCount();
}
function getColCount(): number {
const currentService = get(IUniverInstanceService);
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColumnCount();
}
function getRowRawVisible(row: number): boolean {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getRowRawVisible(row);
}
function getColVisible(col: number): boolean {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
return worksheet.getColVisible(col);
}
function selectRow(rowStart: number, rowEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endColumn = getColCount() - 1;
selectionManagerService.addSelections([
{
range: { startRow: rowStart, startColumn: 0, endColumn, endRow: rowEnd, rangeType: RANGE_TYPE.ROW },
primary: {
startRow: rowStart,
endRow: rowEnd,
startColumn: 0,
endColumn,
actualColumn: 0,
actualRow: rowStart,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
function selectColumn(columnStart: number, columnEnd: number): void {
const selectionManagerService = get(SheetsSelectionsService);
const endRow = getRowCount() - 1;
selectionManagerService.addSelections([
{
range: {
startRow: 0,
startColumn: columnStart,
endColumn: columnEnd,
endRow,
rangeType: RANGE_TYPE.COLUMN,
},
primary: {
startRow: 0,
endRow,
startColumn: columnStart,
endColumn: columnEnd,
actualColumn: columnStart,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
}
describe('hide / unhide rows', () => {
it('should work', async () => {
expect(getRowRawVisible(0)).toBeTruthy();
selectRow(0, 0);
await commandService.executeCommand(SetRowHiddenCommand.id);
expect(getRowRawVisible(0)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getRowRawVisible(0)).toBeTruthy();
await commandService.executeCommand(RedoCommand.id);
expect(getRowRawVisible(0)).toBeFalsy();
// Specify the parameter ranges as the third row
await commandService.executeCommand(SetRowHiddenCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [{ startRow: 2, startColumn: 2, endRow: 2, endColumn: 2, rangeType: RANGE_TYPE.ROW }],
});
expect(getRowRawVisible(2)).toBeFalsy();
// select a range and invoke unhide command will unhide all
// hidden rows in the selected range
selectRow(0, 7);
await commandService.executeCommand(SetSelectedRowsVisibleCommand.id);
expect(getRowRawVisible(0)).toBeTruthy();
expect(getRowRawVisible(2)).toBeTruthy();
await commandService.executeCommand(UndoCommand.id);
expect(getRowRawVisible(0)).toBeFalsy();
expect(getRowRawVisible(2)).toBeFalsy();
});
it('should skip over already hidden rows', async () => {
expect(getRowRawVisible(0)).toBeTruthy();
selectRow(2, 2);
await commandService.executeCommand(SetRowHiddenCommand.id);
expect(getRowRawVisible(2)).toBeFalsy();
selectRow(1, 4);
await commandService.executeCommand(SetRowHiddenCommand.id);
expect(getRowRawVisible(1)).toBeFalsy();
expect(getRowRawVisible(2)).toBeFalsy();
expect(getRowRawVisible(3)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getRowRawVisible(1)).toBeTruthy();
expect(getRowRawVisible(2)).toBeFalsy();
expect(getRowRawVisible(3)).toBeTruthy();
selectRow(1, 2);
await commandService.executeCommand(SetRowHiddenCommand.id);
expect(getRowRawVisible(1)).toBeFalsy();
expect(getRowRawVisible(2)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getRowRawVisible(1)).toBeTruthy();
expect(getRowRawVisible(2)).toBeFalsy();
});
});
describe('hide / unhide columns', () => {
it('should work', async () => {
expect(getColVisible(0)).toBeTruthy();
selectColumn(0, 0);
await commandService.executeCommand(SetColHiddenCommand.id);
expect(getColVisible(0)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getColVisible(0)).toBeTruthy();
await commandService.executeCommand(RedoCommand.id);
expect(getColVisible(0)).toBeFalsy();
// Specify the parameter ranges as the third column
await commandService.executeCommand(SetColHiddenCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
ranges: [{ startRow: 2, startColumn: 2, endRow: 2, endColumn: 2, rangeType: RANGE_TYPE.COLUMN }],
});
expect(getColVisible(2)).toBeFalsy();
// select a range and invoke unhide command will unhide all
// hidden cols in the selected range
selectColumn(0, 7);
await commandService.executeCommand(SetSelectedColsVisibleCommand.id);
expect(getColVisible(0)).toBeTruthy();
expect(getColVisible(2)).toBeTruthy();
await commandService.executeCommand(UndoCommand.id);
expect(getColVisible(0)).toBeFalsy();
expect(getColVisible(2)).toBeFalsy();
});
it('should skip over already hidden cols', async () => {
expect(getColVisible(0)).toBeTruthy();
selectColumn(2, 2);
await commandService.executeCommand(SetColHiddenCommand.id);
expect(getColVisible(2)).toBeFalsy();
selectColumn(1, 4);
await commandService.executeCommand(SetColHiddenCommand.id);
expect(getColVisible(1)).toBeFalsy();
expect(getColVisible(2)).toBeFalsy();
expect(getColVisible(3)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getColVisible(1)).toBeTruthy();
expect(getColVisible(2)).toBeFalsy();
expect(getColVisible(3)).toBeTruthy();
selectColumn(1, 2);
await commandService.executeCommand(SetColHiddenCommand.id);
expect(getColVisible(1)).toBeFalsy();
expect(getColVisible(2)).toBeFalsy();
await commandService.executeCommand(UndoCommand.id);
expect(getColVisible(1)).toBeTruthy();
expect(getColVisible(2)).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,155 @@
/**
* 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, IWorkbookData, Univer, Workbook } from '@univerjs/core';
import type { ISetRowDataCommandParams } from '../set-row-data.command';
import { BooleanNumber, ICommandService, IUniverInstanceService, LocaleType, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetRowDataMutation } from '../../mutations/set-row-data.mutation';
import { SetRowDataCommand } from '../set-row-data.command';
import { createCommandTestBed } from './create-command-test-bed';
const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => {
return {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 'A1',
},
1: {
v: 'A2',
},
},
},
columnData: {
1: {
hd: BooleanNumber.FALSE,
},
},
rowData: {
1: {
hd: BooleanNumber.FALSE,
},
5: {
custom: {
key: 'value',
},
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
};
describe('test set row data commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
function getRowData(row: number) {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
return worksheet.getRowManager().getRow(row);
}
beforeEach(() => {
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA_DEMO());
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetRowDataCommand);
commandService.registerCommand(SetRowDataMutation);
});
afterEach(() => univer.dispose());
it('should set properties on rows', async () => {
expect(getRowData(1)).toEqual({ hd: BooleanNumber.FALSE });
const params: ISetRowDataCommandParams = {
unitId: 'test',
subUnitId: 'sheet1',
rowData: {
1: {
custom: {
color: 'red',
},
},
2: {
custom: {
color: 'green',
},
},
5: {
custom: undefined,
},
},
};
await commandService.executeCommand(SetRowDataCommand.id, params);
expect(getRowData(1)).toEqual({
hd: BooleanNumber.FALSE,
custom: {
color: 'red',
},
});
expect(getRowData(2)).toEqual({
custom: {
color: 'green',
},
});
expect(getRowData(5)).toEqual({
custom: undefined,
});
await commandService.executeCommand(UndoCommand.id);
expect(getRowData(1)).toEqual({
hd: BooleanNumber.FALSE,
});
expect(getRowData(2)).toBeUndefined();
expect(getRowData(5)).toEqual({
custom: {
key: 'value',
},
});
await commandService.executeCommand(RedoCommand.id);
expect(getRowData(1)).toEqual({
hd: BooleanNumber.FALSE,
custom: {
color: 'red',
},
});
expect(getRowData(2)).toEqual({
custom: {
color: 'green',
},
});
expect(getRowData(5)).toEqual({
custom: undefined,
});
});
});

View File

@@ -0,0 +1,200 @@
/**
* 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, Nullable, Univer, Workbook } from '@univerjs/core';
import type { IDeltaRowHeightCommandParams, ISetRowHeightCommandParams } from '../set-worksheet-row-height.command';
import {
BooleanNumber,
ICommandService,
IUniverInstanceService,
RANGE_TYPE,
RedoCommand,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import {
SetWorksheetRowHeightMutation,
SetWorksheetRowIsAutoHeightMutation,
} from '../../mutations/set-worksheet-row-height.mutation';
import {
DeltaRowHeightCommand,
SetRowHeightCommand,
SetWorksheetRowIsAutoHeightCommand,
} from '../set-worksheet-row-height.command';
import { createCommandTestBed } from './create-command-test-bed';
// tests in this file is almost a transpose of packages/base-sheets/src/commands/commands/__tests__/set--width.command.spec.ts
describe('Test set row height commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
function getRowHeight(row: number): number {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
return worksheet.getRowHeight(row);
}
function getRowIsAutoHeight(row: number): Nullable<BooleanNumber> {
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const rowManager = worksheet.getRowManager();
const rowInfo = rowManager.getRow(row);
return rowInfo?.ia;
}
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(DeltaRowHeightCommand);
commandService.registerCommand(SetRowHeightCommand);
commandService.registerCommand(SetWorksheetRowIsAutoHeightCommand);
commandService.registerCommand(SetWorksheetRowHeightMutation);
commandService.registerCommand(SetWorksheetRowIsAutoHeightMutation);
const worksheet = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()!;
const maxColumn = worksheet.getMaxColumns() - 1;
const selectionManager = get(SheetsSelectionsService);
// select row 2, 3
selectionManager.addSelections([
{
range: { startRow: 1, startColumn: 0, endRow: 2, endColumn: maxColumn, rangeType: RANGE_TYPE.ROW },
primary: {
startRow: 1,
startColumn: 0,
endColumn: 0,
endRow: 1,
actualColumn: 0,
actualRow: 1,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
// and continue select row 5
selectionManager.addSelections([
{
range: { startRow: 5, startColumn: 0, endColumn: maxColumn, endRow: 5, rangeType: RANGE_TYPE.ROW },
primary: null,
style: null,
},
]);
});
afterEach(() => univer.dispose());
describe('Delta row heights by dragging', () => {
it('Should change all row selections when anchor row is selected', async () => {
expect(getRowHeight(1)).toBe(24);
await commandService.executeCommand<IDeltaRowHeightCommandParams>(DeltaRowHeightCommand.id, {
deltaY: -5,
anchorRow: 5,
});
expect(getRowHeight(1)).toBe(19);
expect(getRowIsAutoHeight(1)).toBe(BooleanNumber.FALSE);
expect(getRowHeight(2)).toBe(19);
expect(getRowIsAutoHeight(2)).toBe(BooleanNumber.FALSE);
expect(getRowHeight(5)).toBe(19);
expect(getRowIsAutoHeight(5)).toBe(BooleanNumber.FALSE);
await commandService.executeCommand(UndoCommand.id);
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(2)).toBe(24);
expect(getRowHeight(5)).toBe(24);
await commandService.executeCommand(RedoCommand.id);
expect(getRowHeight(1)).toBe(19);
expect(getRowHeight(2)).toBe(19);
expect(getRowHeight(5)).toBe(19);
});
it('Should expand only the anchor row in other situations', async () => {
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(7)).toBe(24);
await commandService.executeCommand<IDeltaRowHeightCommandParams>(DeltaRowHeightCommand.id, {
deltaY: -5,
anchorRow: 7,
});
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(2)).toBe(24);
expect(getRowHeight(5)).toBe(24);
expect(getRowHeight(7)).toBe(19);
});
});
it('Direct change row heights', async () => {
expect(getRowHeight(1)).toBe(24);
await commandService.executeCommand<ISetRowHeightCommandParams>(SetRowHeightCommand.id, {
value: 77,
});
expect(getRowHeight(1)).toBe(77);
expect(getRowIsAutoHeight(1)).toBe(BooleanNumber.FALSE);
expect(getRowHeight(2)).toBe(77);
expect(getRowIsAutoHeight(2)).toBe(BooleanNumber.FALSE);
expect(getRowHeight(5)).toBe(77);
expect(getRowIsAutoHeight(5)).toBe(BooleanNumber.FALSE);
await commandService.executeCommand(UndoCommand.id);
expect(getRowHeight(1)).toBe(24);
expect(getRowHeight(2)).toBe(24);
expect(getRowHeight(5)).toBe(24);
await commandService.executeCommand(RedoCommand.id);
expect(getRowHeight(1)).toBe(77);
expect(getRowHeight(2)).toBe(77);
expect(getRowHeight(5)).toBe(77);
});
it('Direct change row heights with ranges', async () => {
await commandService.executeCommand<ISetRowHeightCommandParams>(SetRowHeightCommand.id, {
value: 88,
ranges: [{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 0 }],
});
expect(getRowHeight(2)).toBe(88);
expect(getRowIsAutoHeight(2)).toBe(BooleanNumber.FALSE);
});
describe('Set fit content in ranges', () => {
it('Should change all rows ia to true in selections', async () => {
await commandService.executeCommand(SetWorksheetRowIsAutoHeightCommand.id);
expect(getRowIsAutoHeight(1)).toBe(BooleanNumber.TRUE);
expect(getRowIsAutoHeight(2)).toBe(BooleanNumber.TRUE);
expect(getRowIsAutoHeight(5)).toBe(BooleanNumber.TRUE);
await commandService.executeCommand(UndoCommand.id);
expect(getRowIsAutoHeight(1)).toBe(undefined);
expect(getRowIsAutoHeight(2)).toBe(undefined);
expect(getRowIsAutoHeight(5)).toBe(undefined);
await commandService.executeCommand(RedoCommand.id);
expect(getRowIsAutoHeight(1)).toBe(BooleanNumber.TRUE);
expect(getRowIsAutoHeight(2)).toBe(BooleanNumber.TRUE);
expect(getRowIsAutoHeight(5)).toBe(BooleanNumber.TRUE);
});
});
});

View File

@@ -0,0 +1,872 @@
/**
* 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 { IColorStyle, Injector, ITextDecoration, ITextRotation, Univer, Workbook } from '@univerjs/core';
import type { ISetStyleCommandParams } from '../set-style.command';
import {
BooleanNumber,
FontItalic,
FontWeight,
HorizontalAlign,
ICommandService,
IUniverInstanceService,
RANGE_TYPE,
RedoCommand,
UndoCommand,
UniverInstanceType,
VerticalAlign,
WrapStrategy,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { InsertSheetCommand } from '../insert-sheet.command';
import {
SetBackgroundColorCommand,
SetBoldCommand,
SetFontFamilyCommand,
SetFontSizeCommand,
SetHorizontalTextAlignCommand,
SetItalicCommand,
SetOverlineCommand,
SetShrinkToFitCommand,
SetStrikeThroughCommand,
SetStyleCommand,
SetTextColorCommand,
SetTextRotationCommand,
SetTextWrapCommand,
SetUnderlineCommand,
SetVerticalTextAlignCommand,
} from '../set-style.command';
import { createCommandTestBed } from './create-command-test-bed';
describe("Test commands used for updating cells' styles", () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetBoldCommand);
commandService.registerCommand(SetItalicCommand);
commandService.registerCommand(SetUnderlineCommand);
commandService.registerCommand(SetStrikeThroughCommand);
commandService.registerCommand(SetOverlineCommand);
commandService.registerCommand(SetFontSizeCommand);
commandService.registerCommand(SetFontFamilyCommand);
commandService.registerCommand(SetTextColorCommand);
commandService.registerCommand(SetBackgroundColorCommand);
commandService.registerCommand(SetVerticalTextAlignCommand);
commandService.registerCommand(SetHorizontalTextAlignCommand);
commandService.registerCommand(SetTextWrapCommand);
commandService.registerCommand(SetShrinkToFitCommand);
commandService.registerCommand(SetTextRotationCommand);
commandService.registerCommand(SetStyleCommand);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
});
afterEach(() => univer.dispose());
it('set array of style', async () => {
const range = { startRow: 1, startColumn: 1, endColumn: 3, endRow: 3, rangeType: RANGE_TYPE.NORMAL };
function getFontColor(row: number, col: number) {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!
.getSheetBySheetId('sheet1')!
.getRange(row, col)
.getFontColor();
}
const defaultColor = '#000000';
const color1 = '#aaa';
const color2 = '#bbb';
const color3 = '#ccc';
const commandParams: ISetStyleCommandParams<IColorStyle[][]> = {
range,
/** Set style by array */
style: {
type: 'cl',
value: [
[{ rgb: color1 }, { rgb: color1 }, { rgb: color1 }],
[{ rgb: color2 }, { rgb: color2 }, { rgb: color2 }],
[{ rgb: color3 }, { rgb: color3 }, { rgb: color3 }],
],
},
};
expect(await commandService.executeCommand(SetStyleCommand.id, commandParams)).toBeTruthy();
expect(getFontColor(0, 0)).toBe(defaultColor);
expect(getFontColor(1, 1)).toBe(color1);
expect(getFontColor(2, 1)).toBe(color2);
expect(getFontColor(3, 1)).toBe(color3);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontColor(1, 1)).toBe(defaultColor);
expect(getFontColor(2, 1)).toBe(defaultColor);
expect(getFontColor(3, 1)).toBe(defaultColor);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontColor(1, 1)).toBe(color1);
expect(getFontColor(2, 1)).toBe(color2);
expect(getFontColor(3, 1)).toBe(color3);
});
describe('bold', () => {
describe('correct situations', () => {
it('will toggle bold style when there is a selected range', async () => {
const selectionManagerService = get(SheetsSelectionsService);
selectionManagerService.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endColumn: 0,
endRow: 0,
actualColumn: 0,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
function getFontBold(): FontWeight | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getFontWeight();
}
expect(await commandService.executeCommand(SetBoldCommand.id)).toBeTruthy();
expect(getFontBold()).toBe(FontWeight.BOLD);
expect(await commandService.executeCommand(SetBoldCommand.id)).toBeTruthy();
expect(getFontBold()).toBe(FontWeight.NORMAL);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontBold()).toBe(FontWeight.BOLD);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontBold()).toBe(FontWeight.NORMAL);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetBoldCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('italic', () => {
describe('correct situations', () => {
it('will toggle italic style when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 1, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endColumn: 0,
endRow: 0,
actualColumn: 0,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
function getFontItalic(
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): FontItalic | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(startRow, startColumn, endRow, endColumn)
.getFontStyle();
}
expect(await commandService.executeCommand(SetItalicCommand.id)).toBeTruthy();
expect(getFontItalic(0, 0, 0, 0)).toBe(FontItalic.ITALIC); // it should work for every cell in selection
expect(getFontItalic(0, 0, 0, 1)).toBe(FontItalic.ITALIC);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontItalic(0, 0, 0, 0)).toBe(FontItalic.NORMAL);
expect(getFontItalic(0, 0, 0, 1)).toBe(FontItalic.NORMAL);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontItalic(0, 0, 0, 0)).toBe(FontItalic.ITALIC);
expect(getFontItalic(0, 0, 0, 1)).toBe(FontItalic.ITALIC);
});
});
});
describe('underline', () => {
describe('correct situations', () => {
it('will toggle underline style when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endColumn: 0,
endRow: 0,
actualColumn: 0,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
function getFontUnderline(): ITextDecoration | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getUnderline();
}
expect(await commandService.executeCommand(SetUnderlineCommand.id)).toBeTruthy();
expect(getFontUnderline()?.s).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetUnderlineCommand.id)).toBeTruthy();
expect(getFontUnderline()?.s).toBe(BooleanNumber.FALSE);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontUnderline()?.s).toBe(BooleanNumber.TRUE);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontUnderline()?.s).toBe(BooleanNumber.FALSE);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetUnderlineCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('strike-through', () => {
describe('correct situations', () => {
it('will toggle strike-through style when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endColumn: 0,
endRow: 0,
actualColumn: 0,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
function getFontThroughLine(): ITextDecoration | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getStrikeThrough();
}
expect(await commandService.executeCommand(SetStrikeThroughCommand.id)).toBeTruthy();
expect(getFontThroughLine()?.s).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetStrikeThroughCommand.id)).toBeTruthy();
expect(getFontThroughLine()?.s).toBe(BooleanNumber.FALSE);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontThroughLine()?.s).toBe(BooleanNumber.TRUE);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontThroughLine()?.s).toBe(BooleanNumber.FALSE);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetStrikeThroughCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('font size', () => {
describe('correct situations', () => {
it('will change font size when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getFontSize(): number | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getFontSize();
}
expect(await commandService.executeCommand(SetFontSizeCommand.id, { value: 18 })).toBeTruthy();
expect(getFontSize()).toBe(18);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontSize()).toBe(11);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontSize()).toBe(18);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetFontSizeCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('font family', () => {
describe('correct situations', () => {
it('will change font family when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getFontFamily(): string | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getFontFamily();
}
expect(await commandService.executeCommand(SetFontFamilyCommand.id, { value: 'Times New Roman' })).toBeTruthy();
expect(getFontFamily()).toBe('Times New Roman');
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontFamily()).toBe('Arial');
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontFamily()).toBe('Times New Roman');
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetFontFamilyCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('font color', () => {
describe('correct situations', () => {
it('will change font color when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endColumn: 0,
endRow: 0,
actualColumn: 0,
actualRow: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
function getFontColor(): string | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getFontColor();
}
function getFontThroughLine(): ITextDecoration | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getStrikeThrough();
}
function getFontUnderline(): ITextDecoration | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getUnderline();
}
function getFontOverline(): ITextDecoration | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getOverline();
}
expect(await commandService.executeCommand(SetStrikeThroughCommand.id)).toBeTruthy();
expect(getFontThroughLine()?.s).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetUnderlineCommand.id)).toBeTruthy();
expect(getFontUnderline()?.s).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetOverlineCommand.id)).toBeTruthy();
expect(getFontOverline()?.s).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetTextColorCommand.id, { value: '#abcdef' })).toBeTruthy();
expect(getFontColor()).toBe('#abcdef');
// You need to ensure that the color of the strike through/underline/overline will also be changed
expect(getFontThroughLine()?.cl?.rgb).toBe('#abcdef');
expect(getFontUnderline()?.cl?.rgb).toBe('#abcdef');
expect(getFontOverline()?.cl?.rgb).toBe('#abcdef');
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getFontColor()).toBe('#000000');
expect(getFontThroughLine()).toStrictEqual({
s: BooleanNumber.TRUE,
}); // no color
expect(getFontUnderline()).toStrictEqual({
s: BooleanNumber.TRUE,
});
expect(getFontOverline()).toStrictEqual({
s: BooleanNumber.TRUE,
});
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getFontColor()).toBe('#abcdef');
expect(getFontThroughLine()?.cl?.rgb).toBe('#abcdef');
expect(getFontUnderline()?.cl?.rgb).toBe('#abcdef');
expect(getFontOverline()?.cl?.rgb).toBe('#abcdef');
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetTextColorCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('background color', () => {
describe('correct situations', () => {
it('will change background color when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getBackgroundColor(): string | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getBackground();
}
expect(
await commandService.executeCommand(SetBackgroundColorCommand.id, { value: '#abcdef' })
).toBeTruthy();
expect(getBackgroundColor()).toBe('#abcdef');
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getBackgroundColor()).toBe('#fff');
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getBackgroundColor()).toBe('#abcdef');
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetBackgroundColorCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('vertical text align', () => {
describe('correct situations', () => {
it('will change vertical text align when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getVerticalAlignment(): VerticalAlign | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getVerticalAlignment();
}
expect(
await commandService.executeCommand(SetVerticalTextAlignCommand.id, { value: VerticalAlign.TOP })
).toBeTruthy();
expect(getVerticalAlignment()).toBe(VerticalAlign.TOP);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getVerticalAlignment()).toBe(VerticalAlign.UNSPECIFIED);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getVerticalAlignment()).toBe(VerticalAlign.TOP);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetVerticalTextAlignCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('horizontal text align', () => {
describe('correct situations', () => {
it('will change horizontal text align when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getHorizontalAlignment(): HorizontalAlign | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getHorizontalAlignment();
}
expect(
await commandService.executeCommand(SetHorizontalTextAlignCommand.id, {
value: HorizontalAlign.RIGHT,
})
).toBeTruthy();
expect(getHorizontalAlignment()).toBe(HorizontalAlign.RIGHT);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getHorizontalAlignment()).toBe(HorizontalAlign.UNSPECIFIED);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getHorizontalAlignment()).toBe(HorizontalAlign.RIGHT);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetHorizontalTextAlignCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('text wrap', () => {
describe('correct situations', () => {
it('will change text wrap when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getTextWrap(): BooleanNumber | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getWrap();
}
expect(
await commandService.executeCommand(SetTextWrapCommand.id, {
value: WrapStrategy.WRAP,
})
).toBeTruthy();
expect(getTextWrap()).toBe(BooleanNumber.TRUE);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getTextWrap()).toBe(BooleanNumber.FALSE);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getTextWrap()).toBe(BooleanNumber.TRUE);
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetTextWrapCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('shrink to fit', () => {
it('changes shrink to fit with undo and redo', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
const getShrinkToFit = () => get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getComposedCellStyle(0, 0)
?.stf;
expect(await commandService.executeCommand(SetShrinkToFitCommand.id, {
value: BooleanNumber.TRUE,
})).toBeTruthy();
expect(getShrinkToFit()).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getShrinkToFit()).toBeUndefined();
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getShrinkToFit()).toBe(BooleanNumber.TRUE);
});
it('toggles shrink to fit when no value is provided', async () => {
get(SheetsSelectionsService).addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: {
startRow: 0,
startColumn: 0,
endRow: 0,
endColumn: 0,
actualRow: 0,
actualColumn: 0,
isMerged: false,
isMergedMainCell: false,
},
style: null,
},
]);
const getShrinkToFit = () => get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getComposedCellStyle(0, 0)
?.stf;
expect(await commandService.executeCommand(SetShrinkToFitCommand.id)).toBeTruthy();
expect(getShrinkToFit()).toBe(BooleanNumber.TRUE);
expect(await commandService.executeCommand(SetShrinkToFitCommand.id)).toBeTruthy();
expect(getShrinkToFit()).toBe(BooleanNumber.FALSE);
});
});
describe('text rotation', () => {
describe('correct situations', () => {
it('will change text rotation when there is a selected range', async () => {
const selectionManager = get(SheetsSelectionsService);
selectionManager.addSelections([
{
range: { startRow: 0, startColumn: 0, endColumn: 0, endRow: 0, rangeType: RANGE_TYPE.NORMAL },
primary: null,
style: null,
},
]);
function getTextRotation(): ITextRotation | undefined {
return get(IUniverInstanceService)
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
?.getSheetBySheetId('sheet1')
?.getRange(0, 0, 0, 0)
.getTextRotation();
}
expect(
await commandService.executeCommand(SetTextRotationCommand.id, {
value: 90,
})
).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 90,
});
expect(
await commandService.executeCommand(SetTextRotationCommand.id, {
value: 'v',
})
).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 0,
v: BooleanNumber.TRUE,
});
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 90,
});
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 0,
v: BooleanNumber.FALSE,
}); // default value
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 90,
});
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getTextRotation()).toStrictEqual({
a: 0,
v: BooleanNumber.TRUE,
});
});
});
describe('fault situations', () => {
it('will not apply when there is no selected ranges', async () => {
const result = await commandService.executeCommand(SetTextRotationCommand.id);
expect(result).toBeFalsy();
});
});
});
describe('set style with specific range', () => {
it('should use the correct unitId and subUnitId when range is provided', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
// Insert a new sheet
expect(await commandService.executeCommand(InsertSheetCommand.id)).toBeTruthy();
const newSheet = workbook.getSheets()[1]; // Get the newly inserted sheet
const unitId = workbook.getUnitId();
const subUnitId = newSheet.getSheetId();
const range = { startRow: 0, startColumn: 0, endColumn: 1, endRow: 1, rangeType: RANGE_TYPE.NORMAL };
const commandParams: ISetStyleCommandParams<IColorStyle> = {
range,
unitId,
subUnitId,
style: {
type: 'bg',
value: { rgb: '#FF0000' },
},
};
expect(await commandService.executeCommand(SetStyleCommand.id, commandParams)).toBeTruthy();
// Verify that the background color was set correctly
const getBackgroundColor = (row: number, col: number) =>
workbook.getSheetBySheetId(subUnitId)!
.getRange(row, col)
.getBackground();
expect(getBackgroundColor(0, 0)).toBe('#FF0000');
expect(getBackgroundColor(0, 1)).toBe('#FF0000');
expect(getBackgroundColor(1, 0)).toBe('#FF0000');
expect(getBackgroundColor(1, 1)).toBe('#FF0000');
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getBackgroundColor(0, 0)).toBe('#fff');
expect(getBackgroundColor(1, 1)).toBe('#fff');
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getBackgroundColor(0, 0)).toBe('#FF0000');
expect(getBackgroundColor(1, 1)).toBe('#FF0000');
});
});
});

View File

@@ -0,0 +1,64 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetTabColorMutation } from '../../mutations/set-tab-color.mutation';
import { SetTabColorCommand } from '../set-tab-color.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test tab color commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetTabColorCommand);
commandService.registerCommand(SetTabColorMutation);
});
afterEach(() => {
univer.dispose();
});
describe('Set several specific colors', () => {
function getTabColor() {
return get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getActiveSheet()?.getTabColor();
}
describe('correct situations', () => {
it('will set tab color', async () => {
expect(await commandService.executeCommand(SetTabColorCommand.id, { value: '#cccccc' })).toBeTruthy();
expect(getTabColor()).toBe('#cccccc');
expect(await commandService.executeCommand(SetTabColorCommand.id, { value: 'red' })).toBeTruthy();
expect(getTabColor()).toBe('red');
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(getTabColor()).toBe('#cccccc');
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(getTabColor()).toBe('red');
});
});
});
});

View File

@@ -0,0 +1,69 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetWorksheetDefaultStyleMutation } from '../../mutations/set-worksheet-default-style.mutation';
import { SetWorksheetDefaultStyleCommand } from '../set-worksheet-default-style.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet default style commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetWorksheetDefaultStyleCommand);
commandService.registerCommand(SetWorksheetDefaultStyleMutation);
});
afterEach(() => {
univer.dispose();
});
describe('set worksheet default style', () => {
it('correct situation', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getSheetBySheetId('sheet1');
if (!workbook) throw new Error('This is an error');
const defaultStyle = { fontSize: 12, fontFamily: 'Arial' };
expect(
await commandService.executeCommand(SetWorksheetDefaultStyleCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
defaultStyle,
})
).toBeTruthy();
expect(worksheet?.getDefaultCellStyle()).toEqual(defaultStyle);
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(worksheet?.getDefaultCellStyle()).toBeUndefined();
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(worksheet?.getDefaultCellStyle()).toEqual(defaultStyle);
});
});
});

View File

@@ -0,0 +1,84 @@
/**
* 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, Workbook } from '@univerjs/core';
import { BooleanNumber, ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { SetWorksheetHideMutation } from '../../mutations/set-worksheet-hide.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { InsertSheetCommand } from '../insert-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { SetWorksheetHideCommand } from '../set-worksheet-hide.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet hide commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetWorksheetHideCommand);
commandService.registerCommand(SetWorksheetHideMutation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetActiveOperation);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
});
afterEach(() => {
univer.dispose();
});
describe('Set worksheet hide', () => {
it('will set current active worksheet hidden', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const targetActiveSheet = workbook.getActiveSheet();
const targetSheetId = targetActiveSheet?.getSheetId();
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(
await commandService.executeCommand(SetWorksheetHideCommand.id, {
subUnitId: targetSheetId,
})
).toBeTruthy();
expect(targetActiveSheet?.getConfig().hidden).toBe(BooleanNumber.TRUE);
// undo
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(targetActiveSheet?.getConfig().hidden).toBe(BooleanNumber.FALSE);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(targetActiveSheet?.getConfig().hidden).toBe(BooleanNumber.TRUE);
});
});
});

View File

@@ -0,0 +1,97 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommandInfo, Injector, Univer, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SetWorksheetNameMutation } from '../../mutations/set-worksheet-name.mutation';
import { SetWorksheetNameCommand } from '../set-worksheet-name.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet name commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetWorksheetNameMutation);
commandService.registerCommand(SetWorksheetNameCommand);
});
afterEach(() => {
univer.dispose();
});
describe('set worksheet name', () => {
describe('set worksheet name', async () => {
it('correct situation: ', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) {
throw new Error('This is an error');
}
expect(
await commandService.executeCommand(SetWorksheetNameCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
name: 'new name',
})
).toBeTruthy();
expect(workbook.getSheetBySheetId('sheet1')?.getConfig().name).toEqual('new name');
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId('sheet1')?.getConfig().name).toEqual('sheet1');
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId('sheet1')?.getConfig().name).toEqual('new name');
});
it('carries the previous name through rename and undo/redo', async () => {
const mutations: ICommandInfo[] = [];
const listener = commandService.onMutationExecutedForCollab((mutation) => {
if (mutation.id === SetWorksheetNameMutation.id) {
mutations.push({ ...mutation, params: { ...mutation.params } });
}
});
try {
expect(await commandService.executeCommand(SetWorksheetNameCommand.id, {
unitId: 'test',
subUnitId: 'sheet1',
name: 'Sheet2',
})).toBe(true);
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
expect(mutations.map((mutation) => mutation.params)).toMatchObject([
{ oldName: 'sheet1', name: 'Sheet2' },
// Undo the rename: Sheet2 -> sheet1.
{ oldName: 'Sheet2', name: 'sheet1' },
// Redo the rename: sheet1 -> Sheet2.
{ oldName: 'sheet1', name: 'Sheet2' },
]);
} finally {
listener.dispose();
}
});
});
});
});

View File

@@ -0,0 +1,87 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { SetWorksheetOrderMutation } from '../../mutations/set-worksheet-order.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { InsertSheetCommand } from '../insert-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { SetWorksheetOrderCommand } from '../set-worksheet-order.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet order commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetWorksheetOrderCommand);
commandService.registerCommand(SetWorksheetOrderMutation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetActiveOperation);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
});
afterEach(() => {
univer.dispose();
});
describe('set worksheet order', () => {
describe('set worksheet order', async () => {
it('correct situation: ', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const targetActiveSheet = workbook.getActiveSheet();
const targetSheetId = targetActiveSheet?.getSheetId();
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(
await commandService.executeCommand(SetWorksheetOrderCommand.id, {
order: 2,
unitId: 'test',
subUnitId: 'sheet1',
})
).toBeTruthy();
expect(workbook.getConfig().sheetOrder.indexOf('sheet1')).toEqual(2);
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getConfig().sheetOrder.indexOf('sheet1')).toEqual(0);
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getConfig().sheetOrder.indexOf('sheet1')).toEqual(2);
});
});
});
});

View File

@@ -0,0 +1,82 @@
/**
* 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, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { InsertSheetMutation } from '../../mutations/insert-sheet.mutation';
import { SetWorksheetHideMutation } from '../../mutations/set-worksheet-hide.mutation';
import { SetWorksheetActiveOperation } from '../../operations/set-worksheet-active.operation';
import { InsertSheetCommand } from '../insert-sheet.command';
import { SetWorksheetActivateCommand } from '../set-worksheet-activate.command';
import { SetWorksheetHideCommand } from '../set-worksheet-hide.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test set worksheet show commands', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(InsertSheetCommand);
commandService.registerCommand(InsertSheetMutation);
commandService.registerCommand(SetWorksheetActiveOperation);
commandService.registerCommand(SetWorksheetActivateCommand);
commandService.registerCommand(SetWorksheetHideCommand);
commandService.registerCommand(SetWorksheetHideMutation);
});
afterEach(() => {
univer.dispose();
});
describe('set sheet show', () => {
describe('set sheet show after make it hidden', async () => {
it('correct situation: ', async () => {
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
if (!workbook) throw new Error('This is an error');
const targetActiveSheet = workbook.getActiveSheet()!;
const targetSheetId = targetActiveSheet?.getSheetId();
expect(
await commandService.executeCommand(SetWorksheetActivateCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(await commandService.executeCommand(InsertSheetCommand.id, {})).toBeTruthy();
expect(
await commandService.executeCommand(SetWorksheetHideCommand.id, { subUnitId: targetSheetId })
).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().hidden).toBeTruthy();
// undo;
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().hidden).toBeFalsy();
// redo
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
expect(workbook.getSheetBySheetId(targetSheetId)?.getConfig().hidden).toBeTruthy();
});
});
});
});

View File

@@ -0,0 +1,829 @@
/**
* 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 } from '@univerjs/core';
import {
CellValueType,
DateSystem,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleType,
ObjectMatrix,
PresetListType,
} from '@univerjs/core';
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '@univerjs/engine-formula';
import { describe, expect, it } from 'vitest';
import { SplitDelimiterEnum } from '../../../basics/split-range-text';
import { IAutoFillService } from '../../../services/auto-fill/auto-fill.service';
import { AUTO_FILL_APPLY_TYPE } from '../../../services/auto-fill/type';
import { INumfmtService } from '../../../services/numfmt/type';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SheetInterceptorService } from '../../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../../skeleton/skeleton.service';
import { InsertColMutation, InsertRowMutation } from '../../mutations/insert-row-col.mutation';
import { RemoveNumfmtMutation, SetNumfmtMutation } from '../../mutations/numfmt.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../../mutations/remove-row-col.mutation';
import { ReorderRangeMutation } from '../../mutations/reorder-range.mutation';
import { SetRangeValuesMutation } from '../../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { AppendRowCommand } from '../append-row.command';
import {
AutoClearContentCommand,
AutoFillCommand,
SheetCopyDownCommand,
SheetCopyRightCommand,
} from '../auto-fill.command';
import { InsertDefinedNameCommand } from '../insert-defined-name.command';
import { RefillCommand } from '../refill.command';
import { ReorderRangeCommand } from '../reorder-range.command';
import { SetRangeValuesCommand } from '../set-range-values.command';
import { SplitTextToColumnsCommand } from '../split-text-to-columns.command';
import { TextToNumberCommand } from '../text-to-number.command';
import { ToggleCellCheckboxCommand } from '../toggle-checkbox.command';
function createAccessor(records: Map<unknown, unknown>): IAccessor {
return {
get: (token: unknown) => records.get(token) as never,
has: (token: unknown) => records.has(token),
} as IAccessor;
}
function createStyles() {
return {
getStyleByCell: (cell?: { s?: unknown }) => typeof cell?.s === 'object' ? cell.s : null,
get: (id: string) => id === 'text-style' ? { n: { pattern: '@' } } : null,
};
}
function createWorkbook(worksheet: unknown) {
return {
getUnitId: () => 'unit-1',
getSheetBySheetId: (subUnitId: string) => subUnitId === 'sheet-1' ? worksheet : null,
getActiveSheet: () => worksheet,
getStyles: createStyles,
getSnapshot: () => ({ locale: LocaleType.EN_US }),
getDateSystem: () => DateSystem.Date1904,
};
}
function createUniverInstanceService(worksheet: unknown) {
const workbook = createWorkbook(worksheet);
return {
getUnit: (unitId: string) => unitId === 'unit-1' ? workbook : null,
getCurrentUnitOfType: () => workbook,
};
}
describe('InsertDefinedNameCommand', () => {
it('creates a defined name and records the remove mutation as undo', () => {
const calls: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const commandService = {
syncExecuteCommand: (id: string, params: unknown) => {
calls.push({ id, params });
return true;
},
};
const undoRedoService = {
pushUndoRedo: (record: unknown) => undoRecords.push(record),
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, undoRedoService],
]));
const params = {
unitId: 'unit-1',
id: 'name-1',
name: 'RevenueTotal',
formulaOrRefString: '=SUM(Sheet1!A:A)',
};
expect(InsertDefinedNameCommand.handler(accessor, params)).toBe(true);
expect(calls).toEqual([{ id: SetDefinedNameMutation.id, params }]);
expect(undoRecords).toEqual([{
unitID: 'unit-1',
undoMutations: [{ id: RemoveDefinedNameMutation.id, params }],
redoMutations: [{ id: SetDefinedNameMutation.id, params }],
}]);
});
it('does not create undo records without params or when the mutation fails', () => {
const undoRecords: unknown[] = [];
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => false }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
]));
expect(InsertDefinedNameCommand.handler(accessor, undefined)).toBe(false);
expect(InsertDefinedNameCommand.handler(accessor, {
unitId: 'unit-1',
id: 'name-1',
name: 'RevenueTotal',
formulaOrRefString: '=SUM(Sheet1!A:A)',
})).toBe(false);
expect(undoRecords).toEqual([]);
});
});
describe('AppendRowCommand', () => {
it('inserts missing sheet rows and columns before appending cell values', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cellMatrix = new ObjectMatrix({ 5: { 0: { v: 'old' } } });
const worksheet = {
getSheetId: () => 'sheet-1',
getCellMatrix: () => cellMatrix,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
const cellValue = { 5: { 0: { v: 'new' }, 3: { v: 'overflow column' } } };
expect(AppendRowCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue,
insertRowNums: 1,
insertColumnNums: 2,
maxRows: 5,
maxColumns: 3,
})).toBe(true);
expect(executed.map((call) => call.id)).toEqual([InsertColMutation.id, InsertRowMutation.id, SetRangeValuesMutation.id]);
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: SetRangeValuesMutation.id },
{ id: RemoveRowMutation.id },
{ id: RemoveColMutation.id },
],
redoMutations: [
{ id: InsertColMutation.id },
{ id: InsertRowMutation.id },
{ id: SetRangeValuesMutation.id },
],
});
});
it('returns false when append mutations do not complete', () => {
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => false }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService({ getSheetId: () => 'sheet-1', getCellMatrix: () => new ObjectMatrix() })],
]));
expect(AppendRowCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: { 0: { 0: { v: 'new' } } },
})).toBe(false);
});
});
describe('SetRangeValuesCommand', () => {
it('sets a two-dimensional value array over the current selection and follows the target range', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cellMatrix = new ObjectMatrix({
1: { 1: { v: 'old-1' }, 2: { v: 'old-2' } },
2: { 1: { v: 'old-3' }, 2: { v: 'old-4' } },
});
const worksheet = {
getSheetId: () => 'sheet-1',
getCellMatrix: () => cellMatrix,
getCellHeight: () => 24,
getMergedCell: () => null,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 } }] }],
[SheetInterceptorService, {
onCommandExecute: () => ({ redos: [{ id: 'interceptor-redo', params: {} }], undos: [{ id: 'interceptor-undo', params: {} }] }),
generateMutationsOfAutoHeight: () => ({ redos: [{ id: 'auto-height-redo', params: {} }], undos: [{ id: 'auto-height-undo', params: {} }] }),
}],
]));
expect(SetRangeValuesCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
value: [[{ v: 'A' }, { v: 'B' }], [{ v: 'C' }, { v: 'D' }]],
redoUndoId: 'set-range-values-test',
})).toBe(true);
expect(executed.map((call) => call.id)).toEqual([SetRangeValuesMutation.id, 'interceptor-redo', 'auto-height-redo']);
expect(executed[0].params).toMatchObject({
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: { 1: { 1: { v: 'A' }, 2: { v: 'B' } }, 2: { 1: { v: 'C' }, 2: { v: 'D' } } },
});
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
id: 'set-range-values-test',
undoMutations: [{ id: SetRangeValuesMutation.id }, { id: 'interceptor-undo' }, { id: 'auto-height-undo' }, { id: SetSelectionsOperation.id }],
redoMutations: [{ id: SetRangeValuesMutation.id }, { id: 'interceptor-redo' }, { id: 'auto-height-redo' }, { id: SetSelectionsOperation.id }],
});
});
it('returns false when no range can be resolved or post mutations fail', () => {
const worksheet = {
getSheetId: () => 'sheet-1',
getCellMatrix: () => new ObjectMatrix(),
getCellHeight: () => undefined,
getMergedCell: () => null,
};
const noRangeAccessor = createAccessor(new Map<unknown, unknown>([
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentSelections: () => [] }],
]));
expect(SetRangeValuesCommand.handler(noRangeAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1', value: { v: 'A' } })).toBe(false);
const failedAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string) => id !== 'interceptor-redo' }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 } }] }],
[SheetInterceptorService, {
onCommandExecute: () => ({ redos: [{ id: 'interceptor-redo', params: {} }], undos: [] }),
generateMutationsOfAutoHeight: () => ({ redos: [], undos: [] }),
}],
]));
expect(SetRangeValuesCommand.handler(failedAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1', value: { v: 'A' } })).toBe(false);
});
});
describe('RefillCommand', () => {
it('delegates the selected auto-fill apply type to the auto-fill service', async () => {
const filledTypes: AUTO_FILL_APPLY_TYPE[] = [];
const autoFillService = {
fillData: (type: AUTO_FILL_APPLY_TYPE) => {
filledTypes.push(type);
return type === AUTO_FILL_APPLY_TYPE.COPY;
},
};
const accessor = createAccessor(new Map<unknown, unknown>([[IAutoFillService, autoFillService]]));
await expect(RefillCommand.handler(accessor, { type: AUTO_FILL_APPLY_TYPE.COPY })).resolves.toBe(true);
await expect(RefillCommand.handler(accessor, { type: AUTO_FILL_APPLY_TYPE.NO_FORMAT })).resolves.toBe(false);
expect(filledTypes).toEqual([AUTO_FILL_APPLY_TYPE.COPY, AUTO_FILL_APPLY_TYPE.NO_FORMAT]);
});
});
describe('AutoFillCommand family', () => {
it('delegates explicit auto-fill ranges to the auto-fill service target', async () => {
const calls: unknown[] = [];
const worksheet = { getSheetId: () => 'sheet-1' };
const autoFillService = {
triggerAutoFill: (...args: unknown[]) => {
calls.push(args);
return Promise.resolve(true);
},
};
const accessor = createAccessor(new Map<unknown, unknown>([
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[IAutoFillService, autoFillService],
]));
const sourceRange = { startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 };
const targetRange = { startRow: 0, endRow: 4, startColumn: 0, endColumn: 1 };
await expect(AutoFillCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', sourceRange, targetRange, applyType: AUTO_FILL_APPLY_TYPE.SERIES })).resolves.toBe(true);
expect(calls).toEqual([['unit-1', 'sheet-1', sourceRange, targetRange, AUTO_FILL_APPLY_TYPE.SERIES]]);
const noTargetAccessor = createAccessor(new Map<unknown, unknown>([
[IUniverInstanceService, { getUnit: () => null }],
[IAutoFillService, autoFillService],
]));
await expect(AutoFillCommand.handler(noTargetAccessor, { unitId: 'missing', subUnitId: 'sheet-1', sourceRange, targetRange })).resolves.toBe(false);
});
it('copy-fills down and right from the current selection and hides the apply menu after success', async () => {
const commandCalls: unknown[] = [];
const shownStates: boolean[] = [];
const worksheet = { getSheetId: () => 'sheet-1' };
const commandService = {
executeCommand: async (id: string, params: unknown) => {
commandCalls.push({ id, params });
return true;
},
};
const autoFillService = {
setShowMenu: (show: boolean) => shownStates.push(show),
};
const selections = [
{ range: { startRow: 3, endRow: 3, startColumn: 0, endColumn: 2 } },
{ range: { startRow: 1, endRow: 3, startColumn: 4, endColumn: 4 } },
];
let selectionIndex = 0;
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[IAutoFillService, autoFillService],
[SheetsSelectionsService, { getCurrentLastSelection: () => selections[selectionIndex++] }],
]));
await expect(SheetCopyDownCommand.handler(accessor)).resolves.toBe(true);
await expect(SheetCopyRightCommand.handler(accessor)).resolves.toBe(true);
expect(commandCalls).toEqual([
{
id: AutoFillCommand.id,
params: {
sourceRange: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 2 },
targetRange: { startRow: 2, endRow: 3, startColumn: 0, endColumn: 2 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
},
},
{
id: AutoFillCommand.id,
params: {
sourceRange: { startRow: 1, endRow: 3, startColumn: 3, endColumn: 3 },
targetRange: { startRow: 1, endRow: 3, startColumn: 3, endColumn: 4 },
unitId: 'unit-1',
subUnitId: 'sheet-1',
applyType: AUTO_FILL_APPLY_TYPE.COPY,
},
},
]);
expect(shownStates).toEqual([false, false]);
});
it('does not copy-fill when the selection cannot produce a source range or the command fails', async () => {
const worksheet = { getSheetId: () => 'sheet-1' };
const accessorAtTopLeft = createAccessor(new Map<unknown, unknown>([
[ICommandService, { executeCommand: async () => true }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[IAutoFillService, { setShowMenu: () => undefined }],
[SheetsSelectionsService, { getCurrentLastSelection: () => ({ range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 } }) }],
]));
await expect(SheetCopyDownCommand.handler(accessorAtTopLeft)).resolves.toBe(false);
await expect(SheetCopyRightCommand.handler(accessorAtTopLeft)).resolves.toBe(false);
const shownStates: boolean[] = [];
const failedAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { executeCommand: async () => false }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[IAutoFillService, { setShowMenu: (show: boolean) => shownStates.push(show) }],
[SheetsSelectionsService, { getCurrentLastSelection: () => ({ range: { startRow: 2, endRow: 4, startColumn: 0, endColumn: 1 } }) }],
]));
await expect(SheetCopyDownCommand.handler(failedAccessor)).resolves.toBe(false);
expect(shownStates).toEqual([]);
});
it('clears dragged content, restores selection on undo, and runs after-interceptor mutations', async () => {
const executed: unknown[] = [];
const undoRecords: unknown[] = [];
const oldSelection = { range: { startRow: 9, endRow: 9, startColumn: 9, endColumn: 9 } };
const cellMatrix = new ObjectMatrix({ 2: { 2: { v: 'old' } } });
const worksheet = {
getSheetId: () => 'sheet-1',
getCellMatrix: () => cellMatrix,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentLastSelection: () => oldSelection }],
[SheetInterceptorService, { afterCommandExecute: () => ({ redos: [{ id: 'after-redo', params: {} }], undos: [{ id: 'after-undo', params: {} }] }) }],
]));
await expect(AutoClearContentCommand.handler(accessor, {
clearRange: { startRow: 2, endRow: 2, startColumn: 2, endColumn: 3 },
selectionRange: { startRow: 2, endRow: 2, startColumn: 2, endColumn: 3 },
})).resolves.toBe(true);
expect((executed[0] as { id: string }).id).toBe(SetRangeValuesMutation.id);
expect((executed[1] as { id: string }).id).toBe(SetSelectionsOperation.id);
expect((executed[2] as { id: string }).id).toBe('after-redo');
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: SetRangeValuesMutation.id },
{ id: SetSelectionsOperation.id, params: { selections: [oldSelection], unitId: 'unit-1', subUnitId: 'sheet-1' } },
{ id: 'after-undo' },
],
});
});
});
describe('TextToNumberCommand', () => {
it('converts numeric text cells and removes text number format where needed', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cellMatrix = new ObjectMatrix({
0: {
0: { v: '42', t: CellValueType.STRING },
1: { v: '003', t: CellValueType.NUMBER, s: 'text-style' },
2: { v: 'abc', t: CellValueType.STRING },
3: { v: '20%', t: CellValueType.FORCE_STRING },
4: { v: '01/02/1904', t: CellValueType.STRING },
},
});
const worksheet = {
getSheetId: () => 'sheet-1',
getCellRaw: (row: number, col: number) => cellMatrix.getValue(row, col),
getCellMatrix: () => cellMatrix,
getStyleDataByHash: (id: string) => id === 'text-style' ? { n: { pattern: '@' } } : null,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[INumfmtService, { getValue: (_unitId: string, _subUnitId: string, row: number, col: number) => row === 0 && col === 1 ? { pattern: '@' } : null }],
]));
expect(TextToNumberCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 4 }],
})).toBe(true);
expect(executed.map((call) => call.id)).toEqual([SetRangeValuesMutation.id, SetNumfmtMutation.id, RemoveNumfmtMutation.id]);
expect(executed[0].params).toMatchObject({
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: {
0: {
0: { v: 42, t: CellValueType.NUMBER },
1: { v: 3, t: CellValueType.NUMBER },
3: { v: 0.2, t: CellValueType.NUMBER },
4: { v: 1, t: CellValueType.NUMBER },
},
},
});
expect(executed[1].params).toMatchObject({
unitId: 'unit-1',
subUnitId: 'sheet-1',
refMap: {
0: { pattern: '0%' },
},
values: {
0: { ranges: [{ startRow: 0, endRow: 0, startColumn: 3, endColumn: 3 }] },
},
});
expect(executed[2].params).toEqual({
unitId: 'unit-1',
subUnitId: 'sheet-1',
ranges: [{ startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }],
});
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: SetRangeValuesMutation.id },
{ id: RemoveNumfmtMutation.id },
{ id: SetNumfmtMutation.id },
],
});
});
it('returns false without a target, without ranges, or when mutation execution fails', () => {
const worksheet = {
getSheetId: () => 'sheet-1',
getCellRaw: () => ({ v: '42', t: CellValueType.STRING }),
getCellMatrix: () => new ObjectMatrix(),
getStyleDataByHash: () => null,
};
const noTargetAccessor = createAccessor(new Map<unknown, unknown>([
[IUniverInstanceService, { getUnit: () => null }],
]));
expect(TextToNumberCommand.handler(noTargetAccessor, { unitId: 'missing', subUnitId: 'sheet-1', ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }] })).toBe(false);
const noRangesAccessor = createAccessor(new Map<unknown, unknown>([
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentSelections: () => [] }],
]));
expect(TextToNumberCommand.handler(noRangesAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(false);
const failedAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => false }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
expect(TextToNumberCommand.handler(failedAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1', ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }] })).toBe(false);
});
it('uses current selections when ranges are omitted', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cellMatrix = new ObjectMatrix({ 2: { 2: { v: '56', t: CellValueType.STRING } } });
const worksheet = {
getSheetId: () => 'sheet-1',
getCellRaw: (row: number, col: number) => cellMatrix.getValue(row, col),
getCellMatrix: () => cellMatrix,
getStyleDataByHash: () => null,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 } }] }],
]));
expect(TextToNumberCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1' })).toBe(true);
expect(executed[0]).toMatchObject({
id: SetRangeValuesMutation.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: { 2: { 2: { v: 56, t: CellValueType.NUMBER } } },
},
});
expect(undoRecords).toHaveLength(1);
});
});
describe('ToggleCellCheckboxCommand', () => {
function createChecklistCell() {
return {
p: {
id: 'doc-1',
body: {
dataStream: 'Alpha\rBeta\r\n',
paragraphs: [
{ startIndex: 5, paragraphId: 'para-1' },
{
startIndex: 10,
paragraphId: 'para-2',
bullet: {
listId: 'check-1',
listType: PresetListType.CHECK_LIST,
nestingLevel: 0,
},
},
],
},
},
};
}
it('toggles a rich-text checklist paragraph and records undo from the old cell value', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cell = createChecklistCell();
const cellMatrix = new ObjectMatrix({ 4: { 3: cell } });
const worksheet = {
getSheetId: () => 'sheet-1',
getCell: (row: number, col: number) => cellMatrix.getValue(row, col),
getCellMatrix: () => cellMatrix,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
expect(ToggleCellCheckboxCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 4,
col: 3,
paragraphIndex: 10,
})).toBe(true);
const redoCell = (executed[0].params as { cellValue: Record<number, Record<number, { t: CellValueType; p: { body: { paragraphs: Array<{ bullet?: { listType: string } }> } } }>> }).cellValue[4][3];
expect(executed[0].id).toBe(SetRangeValuesMutation.id);
expect(redoCell.t).toBe(CellValueType.STRING);
expect(redoCell.p.body.paragraphs[1].bullet?.listType).toBe(PresetListType.CHECK_LIST_CHECKED);
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
redoMutations: [{ id: SetRangeValuesMutation.id }],
undoMutations: [{ id: SetRangeValuesMutation.id }],
});
});
it('returns false when checkbox toggle has no target rich text or paragraph', () => {
const worksheetWithoutRichText = {
getSheetId: () => 'sheet-1',
getCell: () => ({ v: 'plain' }),
getCellMatrix: () => new ObjectMatrix(),
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => true }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService(worksheetWithoutRichText)],
]));
expect(ToggleCellCheckboxCommand.handler(accessor, undefined as never)).toBe(false);
expect(ToggleCellCheckboxCommand.handler(accessor, { unitId: 'missing', subUnitId: 'sheet-1', row: 0, col: 0, paragraphIndex: 0 })).toBe(false);
expect(ToggleCellCheckboxCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', row: 0, col: 0, paragraphIndex: 0 })).toBe(false);
const cell = createChecklistCell();
const cellMatrix = new ObjectMatrix({ 4: { 3: cell } });
const invalidParagraphAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => true }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService({
getSheetId: () => 'sheet-1',
getCell: (row: number, col: number) => cellMatrix.getValue(row, col),
getCellMatrix: () => cellMatrix,
})],
]));
expect(ToggleCellCheckboxCommand.handler(invalidParagraphAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1', row: 4, col: 3, paragraphIndex: 999 })).toBe(false);
});
});
describe('SplitTextToColumnsCommand', () => {
it('splits single-column text into adjacent columns and inserts columns when output exceeds sheet width', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const cellMatrix = new ObjectMatrix({
0: { 1: { v: 'North,West,East', t: CellValueType.STRING } },
1: { 1: { v: '1,2,3', t: CellValueType.STRING } },
});
const worksheet = {
getSheetId: () => 'sheet-1',
getColumnCount: () => 3,
getRowCount: () => 10,
getMaxColumns: () => 3,
getMaxRows: () => 10,
getCell: (row: number, col: number) => cellMatrix.getValue(row, col),
getCellMatrix: () => cellMatrix,
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
} }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
expect(SplitTextToColumnsCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
range: { startRow: 0, endRow: 1, startColumn: 1, endColumn: 1 },
delimiter: SplitDelimiterEnum.Comma,
})).toBe(true);
expect(executed.map((call) => call.id)).toEqual(['sheet.mutation.insert-col', SetRangeValuesMutation.id]);
expect(executed[1].params).toMatchObject({
unitId: 'unit-1',
subUnitId: 'sheet-1',
cellValue: {
0: {
1: { v: 'North' },
2: { v: 'West' },
3: { v: 'East' },
},
1: {
1: { v: '1' },
2: { v: '2' },
3: { v: '3' },
},
},
});
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [{ id: SetRangeValuesMutation.id }, { id: 'sheet.mutation.remove-col' }],
});
});
it('returns false for multi-column source ranges, missing targets, or failed mutation sequence', () => {
const worksheet = {
getSheetId: () => 'sheet-1',
getColumnCount: () => 5,
getRowCount: () => 10,
getMaxColumns: () => 5,
getMaxRows: () => 10,
getCell: () => ({ v: 'A,B', t: CellValueType.STRING }),
getCellMatrix: () => new ObjectMatrix(),
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => true }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
expect(() => SplitTextToColumnsCommand.handler(accessor, { unitId: 'unit-1', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 } })).toThrowError('The range must be in the same column.');
const noTargetAccessor = createAccessor(new Map<unknown, unknown>([[IUniverInstanceService, { getUnit: () => null }]]));
expect(SplitTextToColumnsCommand.handler(noTargetAccessor, { unitId: 'missing', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 } })).toBe(false);
const failedAccessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => false }],
[IUndoRedoService, { pushUndoRedo: () => undefined }],
[IUniverInstanceService, createUniverInstanceService(worksheet)],
]));
expect(SplitTextToColumnsCommand.handler(failedAccessor, { unitId: 'unit-1', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 } })).toBe(false);
});
});
describe('ReorderRangeCommand', () => {
it('executes reorder with interceptor and auto-height mutations and records undo/redo', () => {
const executed: Array<{ id: string; params: unknown }> = [];
const undoRecords: unknown[] = [];
const commandService = {
syncExecuteCommand: (id: string, params: unknown) => {
executed.push({ id, params });
return true;
},
};
const sheetInterceptorService = {
onCommandExecute: () => ({
preRedos: [{ id: 'pre-redo', params: { stage: 'before' } }],
redos: [{ id: 'redo-after-reorder', params: { stage: 'during' } }],
preUndos: [{ id: 'pre-undo', params: { stage: 'before' } }],
undos: [{ id: 'undo-after-reorder', params: { stage: 'during' } }],
}),
afterCommandExecute: () => ({
redos: [{ id: 'post-redo', params: { stage: 'after' } }],
undos: [{ id: 'post-undo', params: { stage: 'after' } }],
}),
generateMutationsOfAutoHeight: () => ({
redos: [{ id: 'auto-height-redo', params: { stage: 'height' } }],
undos: [{ id: 'auto-height-undo', params: { stage: 'height' } }],
}),
};
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, commandService],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[SheetInterceptorService, sheetInterceptorService],
[SheetSkeletonService, { getSkeleton: () => null }],
]));
const range = { startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 };
expect(ReorderRangeCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
range,
order: { 0: 2, 1: 0, 2: 1 },
})).toBe(true);
expect(executed).toEqual([
{ id: 'pre-redo', params: { stage: 'before' } },
{ id: ReorderRangeMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', range, order: { 0: 2, 1: 0, 2: 1 } } },
{ id: 'redo-after-reorder', params: { stage: 'during' } },
{ id: 'post-redo', params: { stage: 'after' } },
{ id: 'auto-height-redo', params: { stage: 'height' } },
]);
expect(undoRecords[0]).toMatchObject({
unitID: 'unit-1',
undoMutations: [
{ id: 'pre-undo' },
{ id: ReorderRangeMutation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-1', range, order: { 0: 1, 1: 2, 2: 0 } } },
{ id: 'undo-after-reorder' },
{ id: 'post-undo' },
{ id: 'auto-height-undo' },
],
redoMutations: [
{ id: 'pre-redo' },
{ id: ReorderRangeMutation.id },
{ id: 'redo-after-reorder' },
{ id: 'post-redo' },
{ id: 'auto-height-redo' },
],
});
});
it('returns false and skips undo records when the reorder sequence fails', () => {
const undoRecords: unknown[] = [];
const accessor = createAccessor(new Map<unknown, unknown>([
[ICommandService, { syncExecuteCommand: () => false }],
[IUndoRedoService, { pushUndoRedo: (record: unknown) => undoRecords.push(record) }],
[SheetInterceptorService, {
onCommandExecute: () => ({ preRedos: [], redos: [], preUndos: [], undos: [] }),
afterCommandExecute: () => ({ redos: [], undos: [] }),
generateMutationsOfAutoHeight: () => ({ redos: [], undos: [] }),
}],
[SheetSkeletonService, { getSkeleton: () => null }],
]));
expect(ReorderRangeCommand.handler(accessor, {
unitId: 'unit-1',
subUnitId: 'sheet-1',
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
order: { 0: 1, 1: 0 },
})).toBe(false);
expect(undoRecords).toEqual([]);
});
});

View File

@@ -0,0 +1,267 @@
/**
* 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 { Dimension, ObjectMatrix, RANGE_TYPE } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { SheetsSelectionsService } from '../../../services/selections/selection.service';
import { SelectionMoveType } from '../../../services/selections/type';
import { SetSelectionsOperation } from '../../operations/selection.operation';
import { AddMergeRedoSelectionsOperationFactory, AddMergeUndoSelectionsOperationFactory } from '../../utils/handle-merge-operation';
import {
DeleteRangeUndoMutationFactory,
handleDeleteRangeMutation,
handleInsertRangeMutation,
InsertRangeUndoMutationFactory,
} from '../../utils/handle-range.mutation';
import { hasOverlappingRanges } from '../../utils/selection-command-util';
import { countCells, getSuitableRangesInView } from '../util';
import {
alignToMergedCellsBorders,
calculateTotalLength,
copyRangeStyles,
copyRangeStylesWithoutBorder,
createRangeIteratorWithSkipFilteredRows,
followSelectionOperation,
getCellAtRowCol,
getPrimaryForRange,
isSingleCellSelection,
setEndForRange,
} from '../utils/selection-utils';
describe('sheets command util', () => {
it('should detect overlapping ranges', () => {
expect(hasOverlappingRanges([
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 },
])).toBe(true);
expect(hasOverlappingRanges([
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
{ startRow: 2, endRow: 3, startColumn: 2, endColumn: 3 },
])).toBe(false);
});
it('should select nearest ranges and split overflow range by max visible row quota', () => {
const ranges = [
{ startRow: 50, endRow: 749, startColumn: 0, endColumn: 2 },
{ startRow: 2000, endRow: 2499, startColumn: 0, endColumn: 2 },
];
const result = getSuitableRangesInView(ranges, {
worksheet: { getColumnCount: () => 10 },
scrollY: 0,
getOffsetRelativeToRowCol: () => ({ row: 100 }),
} as any);
expect(result.suitableRanges).toEqual([
{ startRow: 50, endRow: 749, startColumn: 0, endColumn: 2 },
{ startRow: 2000, endRow: 2299, startColumn: 0, endColumn: 2 },
]);
expect(result.remainingRanges).toEqual([
{ startRow: 2300, endRow: 2499, startColumn: 0, endColumn: 2 },
]);
});
it('should return original ranges when skeleton is absent', () => {
const ranges = [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 }];
const result = getSuitableRangesInView(ranges, null);
expect(result).toEqual({ suitableRanges: ranges, remainingRanges: [] });
});
it('should count matrix cells with values only', () => {
const matrix = new ObjectMatrix<number>();
matrix.setValue(0, 0, 1);
matrix.setValue(1, 2, 2);
matrix.setValue(10, 4, 3);
expect(countCells(matrix)).toBe(3);
});
it('should generate merge selection redo and undo operations from the current primary cell', () => {
const currentSelections = [
{
range: { startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
primary: { actualRow: 1, actualColumn: 1 },
},
];
const accessor = {
get: (token: unknown) => token === SheetsSelectionsService ? { getCurrentSelections: () => currentSelections } : null,
} as any;
const params = {
unitId: 'unit-1',
subUnitId: 'sheet-1',
value: Dimension.ROWS,
selections: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }],
};
expect(AddMergeRedoSelectionsOperationFactory(accessor, params as any, [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 4 }])).toEqual({
id: SetSelectionsOperation.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
type: SelectionMoveType.ONLY_SET,
selections: [{
range: currentSelections[0].range,
style: null,
primary: {
startRow: 1,
startColumn: 1,
endRow: 1,
endColumn: 4,
actualRow: 1,
actualColumn: 1,
isMerged: true,
isMergedMainCell: true,
},
}],
},
});
expect(AddMergeUndoSelectionsOperationFactory(accessor, params as any)).toEqual({
id: SetSelectionsOperation.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
type: SelectionMoveType.ONLY_SET,
selections: currentSelections,
},
});
expect(AddMergeRedoSelectionsOperationFactory({ get: () => ({ getCurrentSelections: () => [] }) } as any, params as any, [])).toBeNull();
expect(AddMergeUndoSelectionsOperationFactory({ get: () => ({ getCurrentSelections: () => [{ range: {}, primary: null }] }) } as any, params as any)).toBeNull();
});
it('should shift object matrix cells when inserting or deleting ranges', () => {
const rowMatrix = new ObjectMatrix<string>({
0: { 0: 'A0' },
1: { 0: 'A1' },
2: { 0: 'A2' },
});
handleInsertRangeMutation(rowMatrix, { startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 }, 2, 0, Dimension.ROWS, { 1: { 0: 'inserted' } });
expect(rowMatrix.getValue(1, 0)).toBe('inserted');
expect(rowMatrix.getValue(2, 0)).toBe('A1');
expect(rowMatrix.getValue(3, 0)).toBe('A2');
handleDeleteRangeMutation(rowMatrix, { startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 }, 3, 0, Dimension.ROWS);
expect(rowMatrix.getValue(1, 0)).toBe('A1');
expect(rowMatrix.getValue(2, 0)).toBe('A2');
const colMatrix = new ObjectMatrix<string>({
0: { 0: 'A', 1: 'B', 2: 'C' },
});
handleInsertRangeMutation(colMatrix, { startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }, 0, 2, Dimension.COLUMNS, { 0: { 1: 'inserted' } });
expect(colMatrix.getValue(0, 1)).toBe('inserted');
expect(colMatrix.getValue(0, 2)).toBe('B');
expect(colMatrix.getValue(0, 3)).toBe('C');
handleDeleteRangeMutation(colMatrix, { startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 }, 0, 3, Dimension.COLUMNS);
expect(colMatrix.getValue(0, 1)).toBe('B');
expect(colMatrix.getValue(0, 2)).toBe('C');
});
it('should build range mutation undo params from worksheet state', () => {
const cellMatrix = new ObjectMatrix({ 0: { 0: { v: 'A' }, 1: { v: 'B' } } });
const worksheet = {
getCellMatrix: () => cellMatrix,
getConfig: () => ({ rowCount: 2, columnCount: 2 }),
};
const accessor = {
get: () => ({
getUnit: () => ({ getSheetBySheetId: () => worksheet }),
}),
} as any;
const params = { unitId: 'unit-1', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, shiftDimension: Dimension.ROWS };
expect(InsertRangeUndoMutationFactory(accessor, params)).toEqual(params);
expect(DeleteRangeUndoMutationFactory(accessor, params)).toMatchObject({
...params,
cellValue: { 0: { 0: { v: 'A' } } },
});
expect(DeleteRangeUndoMutationFactory({ get: () => ({ getUnit: () => null }) } as any, params)).toBeNull();
});
it('should align selection helpers to merged cells and worksheet bounds', () => {
const mergedMatrix = new ObjectMatrix({ 0: { 0: { rowSpan: 2, colSpan: 2 } } });
const worksheet = {
getMatrixWithMergedCells: () => mergedMatrix,
getMergedCell: () => ({ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }),
};
expect(alignToMergedCellsBorders({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, worksheet as any)).toMatchObject({
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
});
expect(getCellAtRowCol(0, 0, worksheet as any)).toEqual({
actualRow: 0,
actualColumn: 0,
startRow: 0,
startColumn: 0,
endRow: 1,
endColumn: 1,
isMerged: true,
isMergedMainCell: true,
rangeType: RANGE_TYPE.NORMAL,
});
expect(getPrimaryForRange({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, worksheet as any)).toMatchObject({
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
actualRow: 0,
actualColumn: 0,
isMerged: true,
isMergedMainCell: true,
});
expect(setEndForRange({ startRow: Number.NaN, endRow: Number.NaN, startColumn: Number.NaN, endColumn: Number.NaN }, 10, 5)).toEqual({
startRow: 0,
endRow: 9,
startColumn: 0,
endColumn: 4,
});
expect(calculateTotalLength({ a: [0, 2], b: [5, 7] })).toBe(8);
expect(isSingleCellSelection({ range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }, primary: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 } } as any)).toBe(true);
expect(isSingleCellSelection(null)).toBe(false);
expect(followSelectionOperation({ startRow: 2, endRow: 2, startColumn: 3, endColumn: 3 }, { getUnitId: () => 'unit-1' } as any, { getSheetId: () => 'sheet-1', getMergedCell: () => null } as any)).toMatchObject({
id: SetSelectionsOperation.id,
params: { unitId: 'unit-1', subUnitId: 'sheet-1', reveal: true },
});
});
it('should iterate only operable cells and copy reusable range styles', () => {
const visited: Array<[number, number]> = [];
const iterator = createRangeIteratorWithSkipFilteredRows({
getRowFiltered: (row: number) => row === 1,
} as any);
iterator.forOperableEach({ startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 }, (row, col) => visited.push([row, col]));
expect(visited).toEqual([[0, 0], [0, 1], [2, 0], [2, 1]]);
const worksheet = {
getCellWithFilteredInterceptors: (row: number, col: number) => {
if (row === 0 && col === 0) return { s: { bg: { rgb: '#fff' } } };
if (row === 0 && col === 1) return { s: {} };
return null;
},
getStyleDataByHash: () => ({ bg: { rgb: '#000' }, bd: { t: { s: 1 } } }),
setStyleData: (style: unknown) => `style:${JSON.stringify(style)}`,
};
expect(copyRangeStyles(worksheet as any, 1, 1, 0, 1, true, 0)).toEqual({
1: { 0: { s: { bg: { rgb: '#fff' } } } },
});
expect(copyRangeStylesWithoutBorder({ ...worksheet, getCellWithFilteredInterceptors: () => ({ s: 'style-hash' }) } as any, 1, 1, 0, 0, true, 0)).toEqual({
1: { 0: { s: 'style:{"bg":{"rgb":"#000"}}' } },
});
});
});

View File

@@ -0,0 +1,71 @@
/**
* 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 { ICommand } from '@univerjs/core';
import type { IRangeProtectionRule } from '../../models/range-protection-rule.model';
import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core';
import { RangeProtectionRuleModel } from '../../models/range-protection-rule.model';
import { AddRangeProtectionMutation } from '../mutations/add-range-protection.mutation';
import { DeleteRangeProtectionMutation } from '../mutations/delete-range-protection.mutation';
export interface IAddRangeProtectionCommandParams {
permissionId: string;
rule: IRangeProtectionRule;
}
export const AddRangeProtectionCommand: ICommand<IAddRangeProtectionCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-range-protection',
async handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const selectionProtectionModel = accessor.get(RangeProtectionRuleModel);
const { rule, permissionId } = params;
const { unitId, subUnitId, ranges, description, viewState, editState } = rule;
const rules = [{
ranges,
permissionId,
id: selectionProtectionModel.createRuleId(unitId, subUnitId),
description,
unitType: rule.unitType,
unitId,
subUnitId,
viewState,
editState,
}];
const result = await commandService.executeCommand(AddRangeProtectionMutation.id, {
unitId,
subUnitId,
rules,
});
if (result) {
const redoMutations = [{ id: AddRangeProtectionMutation.id, params: { unitId, subUnitId, rules } }];
const undoMutations = [{ id: DeleteRangeProtectionMutation.id, params: { unitId, subUnitId, ruleIds: rules.map((rule) => rule.id) } }];
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
}
return true;
},
};

View File

@@ -0,0 +1,424 @@
/**
* 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,
ICommand,
IMutationInfo,
Injector,
IRange,
Nullable,
Worksheet,
} from '@univerjs/core';
import type {
IAddWorksheetMergeMutationParams,
IRemoveWorksheetMergeMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { LocaleKey } from '../../locale/types';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import {
CellModeEnum,
CommandType,
Dimension,
ICommandService,
IConfirmService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
ObjectMatrix,
Rectangle,
sequenceExecute,
Tools,
} from '@univerjs/core';
import { getAddMergeMutationRangeByType } from '../../controllers/merge-cell.controller';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { AddMergeUndoMutationFactory, AddWorksheetMergeMutation } from '../mutations/add-worksheet-merge.mutation';
import {
RemoveMergeUndoMutationFactory,
RemoveWorksheetMergeMutation,
} from '../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import {
AddMergeRedoSelectionsOperationFactory,
AddMergeUndoSelectionsOperationFactory,
} from '../utils/handle-merge-operation';
import { RemoveWorksheetMergeCommand } from './remove-worksheet-merge.command';
import { getSheetCommandTarget } from './utils/target-util';
export interface IAddMergeCommandParams extends ISheetCommandSharedParams {
value?: Dimension.ROWS | Dimension.COLUMNS;
selections: IRange[];
defaultMerge?: boolean;
}
export enum MergeType {
MergeAll = 'mergeAll',
MergeVertical = 'mergeVertical',
MergeHorizontal = 'mergeHorizontal',
}
export interface IMergeCellsUtilOptions {
/**
* Whether to use the default merge behavior when there are existing cell contents in the merge ranges.
* If true, only the value in the upper left cell is retained.
* If false, a confirm dialog will be shown to the user.
* @default true
*/
defaultMerge?: boolean;
/**
* Whether to force merge even if there are existing merged cells that overlap with the new merge ranges.
* If true, the overlapping merged cells will be removed before performing the new merge.
* @default false
*/
isForceMerge?: boolean;
}
function checkCellContentInRanges(worksheet: Worksheet, ranges: IRange[]): boolean {
return ranges.some((range) => checkCellContentInRange(worksheet, range));
}
function checkCellContentInRange(worksheet: Worksheet, range: IRange): boolean {
const { startRow, startColumn, endColumn, endRow } = range;
const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn);
let someCellGoingToBeRemoved = false;
cellMatrix.forValue((row, col, cellData) => {
if (cellData && (row !== startRow || col !== startColumn) && worksheet.cellHasValue(cellData)) {
someCellGoingToBeRemoved = true;
return false;
}
});
return someCellGoingToBeRemoved;
}
export function getClearContentMutationParamsForRanges(
accessor: IAccessor,
unitId: string,
worksheet: Worksheet,
ranges: IRange[]
): { undos: IMutationInfo[]; redos: IMutationInfo[] } {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const subUnitId = worksheet.getSheetId();
// Use the following file as a reference.
// packages/sheets/src/commands/commands/clear-selection-all.command.ts
// packages/sheets/src/commands/mutations/set-range-values.mutation.ts
ranges.forEach((range) => {
const redoMatrix = getClearContentMutationParamForRange(worksheet, range);
const redoMutationParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: redoMatrix.clone(),
};
const undoMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
redoMutationParams
);
undos.push({ id: SetRangeValuesMutation.id, params: undoMutationParams });
redos.push({ id: SetRangeValuesMutation.id, params: redoMutationParams });
});
return {
undos,
redos,
};
}
export function getClearContentMutationParamForRange(worksheet: Worksheet, range: IRange): ObjectMatrix<Nullable<ICellData>> {
const { startRow, startColumn, endColumn, endRow } = range;
const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn, CellModeEnum.Raw);
const redoMatrix = new ObjectMatrix<Nullable<ICellData>>();
let leftTopCellValue: Nullable<ICellData> = null;
cellMatrix.forValue((row, col, cellData) => {
if (cellData && row >= startRow && col >= startColumn) {
if (!leftTopCellValue && worksheet.cellHasValue(cellData) && (cellData.v !== '' || (cellData.p?.body?.dataStream?.length ?? 0) > 2)) {
leftTopCellValue = cellData;
}
redoMatrix.setValue(
row,
col,
cellData.s
? {
v: null,
t: null,
f: null,
si: null,
p: null,
s: cellData.s,
}
: null
);
}
});
redoMatrix.setValue(startRow, startColumn, leftTopCellValue);
return redoMatrix;
}
export const AddWorksheetMergeCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.add-worksheet-merge',
// eslint-disable-next-line max-lines-per-function
handler: async (accessor: IAccessor, params: IAddMergeCommandParams) => {
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { worksheet } = target;
const { unitId, subUnitId, selections } = params;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const ranges = getAddMergeMutationRangeByType(selections, params.value);
// First we should check if there are values in the going-to-be-merged cells.
const willClearSomeCell = checkCellContentInRanges(worksheet, ranges);
if (willClearSomeCell && !params.defaultMerge) {
const confirmService = accessor.get(IConfirmService);
const localeService = accessor.get(LocaleService);
const result = await confirmService.confirm({
id: 'merge.confirm.add-worksheet-merge',
title: {
title: 'sheets.merge.confirm.warning',
},
children: {
title: 'sheets.merge.confirm.title',
},
cancelText: localeService.t<LocaleKey>('sheets.merge.confirm.cancel'),
confirmText: localeService.t<LocaleKey>('sheets.merge.confirm.confirm'),
});
if (!result) return false;
}
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
// prepare redo mutations
const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = {
unitId,
subUnitId,
ranges: Tools.deepClone(ranges),
};
const addMergeMutationParams: IAddWorksheetMergeMutationParams = {
unitId,
subUnitId,
ranges: Tools.deepClone(ranges),
};
// prepare undo mutations
const undoRemoveMergeMutationParams = RemoveMergeUndoMutationFactory(accessor, removeMergeMutationParams);
const undoMutationParams = AddMergeUndoMutationFactory(accessor, addMergeMutationParams);
// params should be the merged cells to be deleted accurately, rather than the selection
if (undoRemoveMergeMutationParams.ranges.length > 0) {
redoMutations.push({ id: RemoveWorksheetMergeMutation.id, params: undoRemoveMergeMutationParams });
}
redoMutations.push({ id: AddWorksheetMergeMutation.id, params: addMergeMutationParams });
undoMutations.push({ id: RemoveWorksheetMergeMutation.id, params: undoMutationParams });
if (undoRemoveMergeMutationParams.ranges.length > 0) {
undoMutations.push({ id: AddWorksheetMergeMutation.id, params: undoRemoveMergeMutationParams });
}
// add set range values mutations to undo redo mutations
if (willClearSomeCell) {
const data = getClearContentMutationParamsForRanges(accessor, unitId, worksheet, ranges);
redoMutations.unshift(...data.redos);
undoMutations.push(...data.undos);
}
const addMergeRedoSelectionsMutation = AddMergeRedoSelectionsOperationFactory(accessor, params, ranges);
addMergeRedoSelectionsMutation && redoMutations.push(addMergeRedoSelectionsMutation);
const addMergeUndoSelectionsMutation = AddMergeUndoSelectionsOperationFactory(accessor, params);
addMergeUndoSelectionsMutation && undoMutations.push(addMergeUndoSelectionsMutation);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const interceptor = sheetInterceptorService.onCommandExecute({
id: AddWorksheetMergeCommand.id,
params: { unitId, subUnitId, ranges },
});
redoMutations.push(...interceptor.redos);
undoMutations.push(...interceptor.undos);
const result = sequenceExecute(redoMutations, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};
export const AddWorksheetMergeAllCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.add-worksheet-merge-all',
handler: async (accessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
const mergeableSelections = getMergeableSelectionsByType(MergeType.MergeAll, selections);
if (!mergeableSelections?.length) {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet } = target;
const unitId = worksheet.getUnitId();
const subUnitId = worksheet.getSheetId();
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(AddWorksheetMergeCommand.id, {
selections: mergeableSelections,
unitId,
subUnitId,
} as IAddMergeCommandParams);
},
};
export const AddWorksheetMergeVerticalCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.add-worksheet-merge-vertical',
handler: async (accessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
const mergeableSelections = getMergeableSelectionsByType(MergeType.MergeVertical, selections);
if (!mergeableSelections?.length) {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet } = target;
const unitId = worksheet.getUnitId();
const subUnitId = worksheet.getSheetId();
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(AddWorksheetMergeCommand.id, {
value: Dimension.COLUMNS,
selections: mergeableSelections,
unitId,
subUnitId,
} as IAddMergeCommandParams);
},
};
export const AddWorksheetMergeHorizontalCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.add-worksheet-merge-horizontal',
handler: async (accessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
const mergeableSelections = getMergeableSelectionsByType(MergeType.MergeHorizontal, selections);
if (!mergeableSelections?.length) {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { workbook, worksheet } = target;
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(AddWorksheetMergeCommand.id, {
value: Dimension.ROWS,
selections: mergeableSelections,
unitId,
subUnitId,
} as IAddMergeCommandParams);
},
};
export function addMergeCellsUtil(injector: Injector, unitId: string, subUnitId: string, ranges: IRange[], options: IMergeCellsUtilOptions = {}) {
const target = getSheetCommandTarget(injector.get(IUniverInstanceService), { unitId, subUnitId });
if (!target) return;
const commandService = injector.get(ICommandService);
const { defaultMerge = true, isForceMerge = false } = options;
const { worksheet } = target;
const mergeData = worksheet.getMergeData();
const overlap = mergeData.some((mergeRange) => {
return ranges.some((range) => {
return Rectangle.intersects(range, mergeRange);
});
});
if (overlap) {
if (!isForceMerge) {
throw new Error('The ranges to be merged overlap with the existing merged cells');
}
commandService.syncExecuteCommand(RemoveWorksheetMergeCommand.id, {
unitId,
subUnitId,
ranges,
});
}
commandService.executeCommand(AddWorksheetMergeCommand.id, {
unitId,
subUnitId,
selections: ranges,
defaultMerge,
});
}
export function getMergeableSelectionsByType(type: MergeType, selections: Nullable<IRange[]>): Nullable<IRange[]> {
if (!selections) return null;
if (type === MergeType.MergeAll) {
return selections.filter((selection) => {
if (selection.startRow === selection.endRow && selection.startColumn === selection.endColumn) {
return false;
}
return true;
});
} else if (type === MergeType.MergeVertical) {
return selections.filter((selection) => {
if (selection.startRow === selection.endRow) {
return false;
}
return true;
});
} else if (type === MergeType.MergeHorizontal) {
return selections.filter((selection) => {
if (selection.startColumn === selection.endColumn) {
return false;
}
return true;
});
}
return selections;
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand } from '@univerjs/core';
import type { IWorksheetProtectionRule } from '../../services/permission/type';
import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core';
import { AddWorksheetProtectionMutation } from '../mutations/add-worksheet-protection.mutation';
import { DeleteWorksheetProtectionMutation } from '../mutations/delete-worksheet-protection.mutation';
export interface IAddWorksheetProtectionParams {
unitId: string;
rule: IWorksheetProtectionRule;
}
export const AddWorksheetProtectionCommand: ICommand<IAddWorksheetProtectionParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.add-worksheet-protection',
async handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { rule, unitId } = params;
const subUnitId = rule.subUnitId;
const result = await commandService.executeCommand(AddWorksheetProtectionMutation.id, {
unitId,
rule,
subUnitId: rule.subUnitId,
});
if (result) {
const redoMutations = [{ id: AddWorksheetProtectionMutation.id, params: { unitId, rule, subUnitId: rule.subUnitId } }];
const undoMutations = [{ id: DeleteWorksheetProtectionMutation.id, params: { unitId, subUnitId } }];
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
}
return true;
},
};

View File

@@ -0,0 +1,55 @@
/**
* 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, ICommand } from '@univerjs/core';
import type { IWorksheetRangeThemeStyleMutationParams } from '../../basics/interfaces/mutation-interface';
import {
CommandType,
ICommandService,
IUndoRedoService,
} from '@univerjs/core';
import {
SetWorksheetRangeThemeStyleMutation,
SetWorksheetRangeThemeStyleMutationFactory,
} from '../mutations/add-worksheet-range-theme.mutation';
import { DeleteWorksheetRangeThemeStyleMutation } from '../mutations/delete-worksheet-range-theme.mutation';
export const SetWorksheetRangeThemeStyleCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.set-worksheet-range-theme-style',
handler: (accessor: IAccessor, params: IWorksheetRangeThemeStyleMutationParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId } = params;
const undoMutationParams = SetWorksheetRangeThemeStyleMutationFactory(accessor, params);
const result = commandService.syncExecuteCommand(SetWorksheetRangeThemeStyleMutation.id, params);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: DeleteWorksheetRangeThemeStyleMutation.id, params: undoMutationParams }],
redoMutations: [{ id: SetWorksheetRangeThemeStyleMutation.id, params }],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,137 @@
/**
* 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, ICommand, IMutationInfo, IObjectMatrixPrimitiveType } from '@univerjs/core';
import type {
IInsertColMutationParams,
IInsertRowMutationParams,
IRemoveColMutationParams,
IRemoveRowsMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import {
CommandType,
ICommandService,
IUndoRedoService,
sequenceExecute,
} from '@univerjs/core';
import {
InsertColMutation,
InsertColMutationUndoFactory,
InsertRowMutation,
InsertRowMutationUndoFactory,
} from '../mutations/insert-row-col.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../mutations/remove-row-col.mutation';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
export interface IAppendRowCommandParams {
unitId: string;
subUnitId: string;
cellValue: IObjectMatrixPrimitiveType<ICellData>;
insertRowNums?: number;
insertColumnNums?: number;
maxRows?: number;
maxColumns?: number;
}
export const AppendRowCommandId = 'sheet.command.append-row';
/**
* this command and its interface should not be exported from index.ts
*
* @internal
*/
export const AppendRowCommand: ICommand<IAppendRowCommandParams> = {
type: CommandType.COMMAND,
id: AppendRowCommandId,
handler: (accessor: IAccessor, params: IAppendRowCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId, subUnitId, cellValue, insertRowNums, insertColumnNums, maxRows, maxColumns } = params;
// Set row values
const setRangeValuesMutationRedoParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue,
};
const setRangeValuesMutationUndoParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
setRangeValuesMutationRedoParams
);
const redoMutations: IMutationInfo[] = [{ id: SetRangeValuesMutation.id, params: setRangeValuesMutationRedoParams }];
const undoMutations: IMutationInfo[] = [{ id: SetRangeValuesMutation.id, params: setRangeValuesMutationUndoParams }];
// If the last row is not empty, insert a row in last
if (insertRowNums) {
// Insert a row in last
const insertRowRedoParams: IInsertRowMutationParams = {
unitId,
subUnitId,
range: {
startRow: maxRows as number,
endRow: maxRows as number,
startColumn: 0,
endColumn: (maxColumns as number) - 1,
},
};
const insertRowUndoParams: IRemoveRowsMutationParams = InsertRowMutationUndoFactory(
accessor,
insertRowRedoParams
);
redoMutations.unshift({ id: InsertRowMutation.id, params: insertRowRedoParams });
undoMutations.push({ id: RemoveRowMutation.id, params: insertRowUndoParams });
}
// If the append row data has more columns than the current sheet, insert columns in last
if (insertColumnNums) {
// Insert columns in last
const insertColRedoParams: IInsertColMutationParams = {
unitId,
subUnitId,
range: {
startRow: 0,
endRow: (maxRows as number) - 1,
startColumn: maxColumns as number,
endColumn: (maxColumns as number) - 1 + insertColumnNums,
},
};
const insertColUndoParams: IRemoveColMutationParams = InsertColMutationUndoFactory(
accessor,
insertColRedoParams
);
redoMutations.unshift({ id: InsertColMutation.id, params: insertColRedoParams });
undoMutations.push({ id: RemoveColMutation.id, params: insertColUndoParams });
}
const result = sequenceExecute(redoMutations, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,236 @@
/**
* 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, ICommand, IRange } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute } from '@univerjs/core';
import { generateNullCellValue } from '../../basics/utils';
import { IAutoFillService } from '../../services/auto-fill/auto-fill.service';
import { AUTO_FILL_APPLY_TYPE } from '../../services/auto-fill/type';
import { SheetsSelectionsService } from '../../services/selections';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../operations/selection.operation';
import { getSheetCommandTarget } from './utils/target-util';
export interface IAutoFillCommandParams extends Partial<ISheetCommandSharedParams> {
sourceRange: IRange;
targetRange: IRange;
applyType?: AUTO_FILL_APPLY_TYPE; // manual apply type
}
export const AutoFillCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.auto-fill',
handler: async (accessor: IAccessor, params: IAutoFillCommandParams) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const { unitId, subUnitId } = target;
const { sourceRange, targetRange, applyType } = params;
const autoFillService = accessor.get(IAutoFillService);
return autoFillService.triggerAutoFill(unitId, subUnitId, sourceRange, targetRange, applyType);
},
};
function getSheetCopyFillRange(accessor: IAccessor, direction: 'down' | 'right'): IAutoFillCommandParams | null {
const selection = accessor.get(SheetsSelectionsService).getCurrentLastSelection();
if (!selection) {
return null;
}
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) {
return null;
}
const { unitId, subUnitId } = target;
const { startRow, endRow, startColumn, endColumn } = selection.range;
if (direction === 'down') {
if (startRow === endRow) {
if (startRow === 0) {
return null;
}
return {
sourceRange: { startRow: startRow - 1, endRow: startRow - 1, startColumn, endColumn },
targetRange: { startRow: startRow - 1, endRow, startColumn, endColumn },
unitId,
subUnitId,
applyType: AUTO_FILL_APPLY_TYPE.COPY,
};
}
return {
sourceRange: { startRow, endRow: startRow, startColumn, endColumn },
targetRange: { startRow, endRow, startColumn, endColumn },
unitId,
subUnitId,
applyType: AUTO_FILL_APPLY_TYPE.COPY,
};
}
if (startColumn === endColumn) {
if (startColumn === 0) {
return null;
}
return {
sourceRange: { startRow, endRow, startColumn: startColumn - 1, endColumn: startColumn - 1 },
targetRange: { startRow, endRow, startColumn: startColumn - 1, endColumn },
unitId,
subUnitId,
applyType: AUTO_FILL_APPLY_TYPE.COPY,
};
}
return {
sourceRange: { startRow, endRow, startColumn, endColumn: startColumn },
targetRange: { startRow, endRow, startColumn, endColumn },
unitId,
subUnitId,
applyType: AUTO_FILL_APPLY_TYPE.COPY,
};
}
async function executeSheetCopyFill(accessor: IAccessor, direction: 'down' | 'right') {
const params = getSheetCopyFillRange(accessor, direction);
if (!params) {
return false;
}
const result = await accessor.get(ICommandService).executeCommand(AutoFillCommand.id, params);
if (result) {
accessor.get(IAutoFillService).setShowMenu(false);
}
return result;
}
export const SheetCopyDownCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.copy-down',
handler: async (accessor: IAccessor) => executeSheetCopyFill(accessor, 'down'),
};
export const SheetCopyRightCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.copy-right',
handler: async (accessor: IAccessor) => executeSheetCopyFill(accessor, 'right'),
};
export interface IAutoClearContentCommand extends ISheetCommandSharedParams {
clearRange: IRange;
selectionRange: IRange;
}
export const AutoClearContentCommand: ICommand = {
id: 'sheet.command.auto-clear-content',
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function
handler: async (accessor: IAccessor, params: IAutoClearContentCommand) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const selectionsService = accessor.get(SheetsSelectionsService);
const { unitId, subUnitId } = target;
const { clearRange, selectionRange } = params;
const { startColumn, startRow } = selectionRange;
const clearMutationParams: ISetRangeValuesMutationParams = {
subUnitId,
unitId,
cellValue: generateNullCellValue([clearRange]),
};
const undoClearMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
clearMutationParams
);
const redos = [
{
id: SetRangeValuesMutation.id,
params: clearMutationParams,
},
{
id: SetSelectionsOperation.id,
params: {
selections: [
{
primary: {
startColumn,
startRow,
endColumn: startColumn,
endRow: startRow,
actualRow: startRow,
actualColumn: startColumn,
isMerged: false,
isMergedMainCell: false,
},
range: {
...selectionRange,
},
},
],
unitId,
subUnitId,
},
},
];
const undos = [
{
id: SetRangeValuesMutation.id,
params: undoClearMutationParams,
},
{
id: SetSelectionsOperation.id,
params: {
selections: [selectionsService.getCurrentLastSelection()],
unitId,
subUnitId,
},
},
];
const result = sequenceExecute(redos, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: SetRangeValuesMutation.id,
params: clearMutationParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
undoRedoService.pushUndoRedo({
// If there are multiple mutations that form an encapsulated project, they must be encapsulated in the same undo redo element.
// Hooks can be used to hook the code of external controllers to add new actions.
unitID: unitId,
undoMutations: [...undos, ...afterInterceptors.undos],
redoMutations: [...redos, ...afterInterceptors.redos],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,115 @@
/**
* 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 { ICommand, IMutationInfo, IRange } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute } from '@univerjs/core';
import { generateNullCell, getVisibleRanges } from '../../basics/utils';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../skeleton/skeleton.service';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { getSuitableRangesInView } from './util';
import { getSheetCommandTarget } from './utils/target-util';
export interface IClearSelectionAllCommandParams {
unitId?: string;
subUnitId?: string;
ranges?: IRange[];
}
/**
* The command to clear all in current selected ranges.
*/
export const ClearSelectionAllCommand: ICommand<IClearSelectionAllCommandParams> = {
id: 'sheet.command.clear-selection-all',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId: params?.unitId, subUnitId: params?.subUnitId });
if (!target) return false;
const selectionManagerService = accessor.get(SheetsSelectionsService);
const { unitId, subUnitId } = target;
const ranges = params?.ranges || selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!ranges?.length) return false;
const sheetSkeletonService = accessor.get(SheetSkeletonService);
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
if (!skeleton) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
// clear style and content
const visibleRanges = getVisibleRanges(ranges, accessor, unitId, subUnitId);
const clearMutationParams: ISetRangeValuesMutationParams = {
subUnitId,
unitId,
cellValue: generateNullCell(visibleRanges),
};
const undoClearMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
clearMutationParams
);
redoMutations.push({
id: SetRangeValuesMutation.id,
params: clearMutationParams,
});
undoMutations.push({
id: SetRangeValuesMutation.id,
params: undoClearMutationParams,
});
// intercept
const intercepted = sheetInterceptorService.onCommandExecute({ id: ClearSelectionAllCommand.id, params });
redoMutations.push(...intercepted.redos);
undoMutations.unshift(...intercepted.undos);
const result = sequenceExecute(redoMutations, commandService);
// auto height
const { suitableRanges, remainingRanges } = getSuitableRangesInView(ranges, skeleton);
const { undos: autoHeightUndos, redos: autoHeightRedos } = sheetInterceptorService.generateMutationsOfAutoHeight({
unitId,
subUnitId,
ranges: suitableRanges,
autoHeightRanges: suitableRanges,
lazyAutoHeightRanges: remainingRanges,
});
const autoHeightExecuteResult = sequenceExecute(autoHeightRedos, commandService);
if (result.result && autoHeightExecuteResult.result) {
redoMutations.push(...autoHeightRedos);
undoMutations.push(...autoHeightUndos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};

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 { ICommand, IMutationInfo, IRange } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute } from '@univerjs/core';
import { generateNullCellValue, getVisibleRanges } from '../../basics/utils';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../skeleton/skeleton.service';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { getSuitableRangesInView } from './util';
import { getSheetCommandTarget } from './utils/target-util';
export interface IClearSelectionContentCommandParams {
unitId?: string;
subUnitId?: string;
ranges?: IRange[];
}
/**
* The command to clear content in current selected ranges.
*/
export const ClearSelectionContentCommand: ICommand<IClearSelectionContentCommandParams> = {
id: 'sheet.command.clear-selection-content',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId: params?.unitId, subUnitId: params?.subUnitId });
if (!target) return false;
const selectionManagerService = accessor.get(SheetsSelectionsService);
const { unitId, subUnitId } = target;
const ranges = params?.ranges || selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!ranges?.length) return false;
const sheetSkeletonService = accessor.get(SheetSkeletonService);
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
if (!skeleton) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
// clear content
const visibleRanges = getVisibleRanges(ranges, accessor, unitId, subUnitId);
const clearMutationParams: ISetRangeValuesMutationParams = {
subUnitId,
unitId,
cellValue: generateNullCellValue(visibleRanges),
};
const undoClearMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
clearMutationParams
);
redoMutations.push({
id: SetRangeValuesMutation.id,
params: clearMutationParams,
});
undoMutations.push({
id: SetRangeValuesMutation.id,
params: undoClearMutationParams,
});
// intercept
const intercepted = sheetInterceptorService.onCommandExecute({ id: ClearSelectionContentCommand.id, params });
redoMutations.push(...intercepted.redos);
undoMutations.unshift(...intercepted.undos);
const result = sequenceExecute(redoMutations, commandService);
// auto height
const { suitableRanges, remainingRanges } = getSuitableRangesInView(ranges, skeleton);
const { undos: autoHeightUndos, redos: autoHeightRedos } = sheetInterceptorService.generateMutationsOfAutoHeight({
unitId,
subUnitId,
ranges: suitableRanges,
autoHeightRanges: suitableRanges,
lazyAutoHeightRanges: remainingRanges,
});
const autoHeightExecuteResult = sequenceExecute(autoHeightRedos, commandService);
if (result.result && autoHeightExecuteResult.result) {
redoMutations.push(...autoHeightRedos);
undoMutations.push(...autoHeightUndos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};

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 { ICommand, IMutationInfo, IRange } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute } from '@univerjs/core';
import { generateNullCellStyle, getVisibleRanges } from '../../basics/utils';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../skeleton/skeleton.service';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { getSuitableRangesInView } from './util';
import { getSheetCommandTarget } from './utils/target-util';
export interface IClearSelectionFormatCommandParams {
unitId?: string;
subUnitId?: string;
ranges?: IRange[];
}
/**
* The command to clear content in current selected ranges.
*/
export const ClearSelectionFormatCommand: ICommand<IClearSelectionFormatCommandParams> = {
id: 'sheet.command.clear-selection-format',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId: params?.unitId, subUnitId: params?.subUnitId });
if (!target) return false;
const selectionManagerService = accessor.get(SheetsSelectionsService);
const { unitId, subUnitId } = target;
const ranges = params?.ranges || selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!ranges?.length) return false;
const sheetSkeletonService = accessor.get(SheetSkeletonService);
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
if (!skeleton) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
// clear style
const visibleRanges = getVisibleRanges(ranges, accessor, unitId, subUnitId);
const clearMutationParams: ISetRangeValuesMutationParams = {
subUnitId,
unitId,
cellValue: generateNullCellStyle(visibleRanges),
};
const undoClearMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
clearMutationParams
);
redoMutations.push({
id: SetRangeValuesMutation.id,
params: clearMutationParams,
});
undoMutations.push({
id: SetRangeValuesMutation.id,
params: undoClearMutationParams,
});
// intercept
const intercepted = sheetInterceptorService.onCommandExecute({ id: ClearSelectionFormatCommand.id, params });
redoMutations.push(...intercepted.redos);
undoMutations.unshift(...intercepted.undos);
const result = sequenceExecute(redoMutations, commandService);
// auto height
const { suitableRanges, remainingRanges } = getSuitableRangesInView(ranges, skeleton);
const { undos: autoHeightUndos, redos: autoHeightRedos } = sheetInterceptorService.generateMutationsOfAutoHeight({
unitId,
subUnitId,
ranges: suitableRanges,
autoHeightRanges: suitableRanges,
lazyAutoHeightRanges: remainingRanges,
});
const autoHeightExecuteResult = sequenceExecute(autoHeightRedos, commandService);
if (result.result && autoHeightExecuteResult.result) {
redoMutations.push(...autoHeightRedos);
undoMutations.push(...autoHeightUndos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,334 @@
/**
* 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,
ICommand,
IMutationInfo,
IObjectMatrixPrimitiveType,
Nullable,
Workbook,
} from '@univerjs/core';
import type {
IInsertSheetMutationParams,
IRemoveSheetMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { IUniverSheetsConfig } from '../../config/config';
import type { LocaleKey } from '../../locale/types';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import {
cloneWorksheetData,
CommandType,
generateRandomId,
ICommandService,
IConfigService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
sequenceExecute,
} from '@univerjs/core';
import { defaultLargeSheetOperationConfig, SHEETS_PLUGIN_CONFIG_KEY } from '../../config/config';
import { SheetLazyExecuteScheduleService } from '../../services/lazy-execute-schedule.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { CopyWorksheetEndMutation } from '../mutations/copy-worksheet-end.mutation';
import { InsertSheetMutation, InsertSheetUndoMutationFactory } from '../mutations/insert-sheet.mutation';
import { RemoveSheetMutation } from '../mutations/remove-sheet.mutation';
import { SetRangeValuesMutation } from '../mutations/set-range-values.mutation';
import { getSheetCommandTarget } from './utils/target-util';
/**
* Count the total number of cells in cellData
*/
function countCells(cellData: IObjectMatrixPrimitiveType<Nullable<ICellData>>): number {
let count = 0;
for (const rowKey of Object.keys(cellData)) {
const rowData = cellData[Number(rowKey)];
if (rowData) {
count += Object.keys(rowData).length;
}
}
return count;
}
/**
* Split cellData into batches for SetRangeValuesMutation
* Returns the first chunk separately (to be included in InsertSheetMutation)
* and remaining chunks (to be scheduled for idle execution)
* @param unitId - The unit ID
* @param subUnitId - The sub unit ID (sheet ID)
* @param cellData - The cell data to split
* @param batchSize - The maximum number of cells per batch
* @returns Object containing firstChunkCellData and remainingMutations
*/
function splitCellDataIntoBatches(
unitId: string,
subUnitId: string,
cellData: IObjectMatrixPrimitiveType<Nullable<ICellData>>,
batchSize: number
): {
firstChunkCellData: IObjectMatrixPrimitiveType<Nullable<ICellData>>;
remainingMutations: IMutationInfo<ISetRangeValuesMutationParams>[];
} {
const batches: IObjectMatrixPrimitiveType<Nullable<ICellData>>[] = [];
let currentBatch: IObjectMatrixPrimitiveType<Nullable<ICellData>> = {};
let cellCount = 0;
for (const rowKey in cellData) {
const row = Number(rowKey);
const rowData = cellData[row];
if (!rowData) continue;
const rowCellCount = Object.keys(rowData).length;
// If adding this row would exceed the batch size, push current batch first
if (cellCount > 0 && cellCount + rowCellCount > batchSize) {
batches.push(currentBatch);
currentBatch = {};
cellCount = 0;
}
// Add entire row at once
currentBatch[row] = rowData;
cellCount += rowCellCount;
// If batch is full, push it
if (cellCount >= batchSize) {
batches.push(currentBatch);
currentBatch = {};
cellCount = 0;
}
}
// Push remaining cells
if (cellCount > 0) {
batches.push(currentBatch);
}
// First chunk goes into InsertSheetMutation
const firstChunkCellData = batches.length > 0 ? batches[0] : {};
// Remaining chunks become SetRangeValuesMutation (for idle scheduling)
const remainingMutations: IMutationInfo<ISetRangeValuesMutationParams>[] = batches.slice(1).map((batch) => ({
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue: batch,
__splitChunk__: true,
},
}));
return { firstChunkCellData, remainingMutations };
}
export interface ICopySheetCommandParams {
unitId?: string;
subUnitId?: string;
}
export interface ICopySheetCommandInterceptorParams extends ICopySheetCommandParams {
targetSubUnitId: string;
copyContext: Map<string, unknown>;
}
const COPY_SHEET_COMMAND_ID = 'sheet.command.copy-sheet';
interface IBuildCopySheetResult {
/** Remaining mutations to be scheduled for idle execution (local only, not serialized) */
scheduledMutations: IMutationInfo<ISetRangeValuesMutationParams>[];
redos: IMutationInfo[];
undos: IMutationInfo[];
unitId: string;
/** New sheet ID for scheduling remaining mutations */
newSheetId: string;
/** Whether the sheet was split into chunks */
isSplit: boolean;
}
// eslint-disable-next-line max-lines-per-function
function buildCopySheetMutations(
accessor: IAccessor,
workbook: Workbook,
worksheet: ReturnType<Workbook['getActiveSheet']>,
unitId: string,
subUnitId: string,
localeService: LocaleService,
sheetInterceptorService: SheetInterceptorService
): IBuildCopySheetResult {
const configService = accessor.get(IConfigService);
const pluginConfig = configService.getConfig<IUniverSheetsConfig>(SHEETS_PLUGIN_CONFIG_KEY);
const largeSheetConfig = {
...defaultLargeSheetOperationConfig,
...pluginConfig?.largeSheetOperation,
};
const config = cloneWorksheetData(worksheet!.getConfig());
config.name = getCopyUniqueSheetName(workbook, localeService, config.name);
const newSheetId = generateRandomId();
config.id = newSheetId;
const sheetIndex = workbook.getSheetIndex(worksheet!);
const { cellData } = config;
const cellCount = countCells(cellData);
// Only split if the cell count exceeds the threshold
const shouldSplit = cellCount >= largeSheetConfig.largeSheetCellCountThreshold;
let insertSheetMutationParams: IInsertSheetMutationParams;
let scheduledMutations: IMutationInfo<ISetRangeValuesMutationParams>[] = [];
if (shouldSplit) {
// Split mode: include first chunk in InsertSheetMutation, schedule the rest
const { firstChunkCellData, remainingMutations } = splitCellDataIntoBatches(
unitId,
newSheetId,
cellData,
largeSheetConfig.batchSize
);
// Insert sheet with first chunk of cell data
const sheetConfigWithFirstChunk = { ...config, cellData: firstChunkCellData as IObjectMatrixPrimitiveType<ICellData> };
insertSheetMutationParams = {
index: sheetIndex + 1,
sheet: sheetConfigWithFirstChunk,
unitId,
};
// Remaining mutations will be scheduled for idle execution
scheduledMutations = remainingMutations;
} else {
// No split: insert sheet with all cell data at once
insertSheetMutationParams = {
index: sheetIndex + 1,
sheet: config,
unitId,
};
}
const removeSheetMutationParams: IRemoveSheetMutationParams = InsertSheetUndoMutationFactory(
accessor,
insertSheetMutationParams
);
const copyContext = new Map<string, unknown>();
const intercepted = sheetInterceptorService.onCommandExecute({
id: COPY_SHEET_COMMAND_ID,
params: { unitId, subUnitId, targetSubUnitId: config.id, copyContext } satisfies ICopySheetCommandInterceptorParams,
});
// Redos only include InsertSheetMutation (with first chunk), remaining mutations are scheduled
const redos: IMutationInfo[] = [
...(intercepted.preRedos ?? []),
{ id: InsertSheetMutation.id, params: insertSheetMutationParams },
...intercepted.redos,
];
// Undo just removes the sheet - all scheduled mutations become irrelevant
const undos: IMutationInfo[] = [
...(intercepted.preUndos ?? []),
{ id: RemoveSheetMutation.id, params: removeSheetMutationParams },
...intercepted.undos,
];
return {
redos,
undos,
unitId,
newSheetId,
isSplit: shouldSplit,
scheduledMutations,
};
}
export const CopySheetCommand: ICommand = {
type: CommandType.COMMAND,
id: COPY_SHEET_COMMAND_ID,
handler: (accessor: IAccessor, params?: ICopySheetCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const localeService = accessor.get(LocaleService);
const sheetLazyExecuteScheduleService = accessor.get(SheetLazyExecuteScheduleService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) {
return false;
}
const { workbook, worksheet, unitId, subUnitId } = target;
const { redos, undos, newSheetId, isSplit, scheduledMutations } = buildCopySheetMutations(
accessor,
workbook,
worksheet,
unitId,
subUnitId,
localeService,
sheetInterceptorService
);
const insertResult = sequenceExecute(redos, commandService).result;
if (insertResult) {
// For split case:
// - Undo: just remove the sheet (scheduled mutations become irrelevant)
// - Redo: empty (don't support redo for large sheets to avoid performance issues)
if (isSplit) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos,
redoMutations: [], // No redo for split case
});
// Schedule remaining mutations for idle execution
if (scheduledMutations.length > 0) {
// Immediately sync all mutations to changeset (syncOnly: true means sync but don't execute)
// The actual execution will happen in SheetLazyExecuteScheduleService with onlyLocal
for (const mutation of scheduledMutations) {
commandService.syncExecuteCommand(mutation.id, mutation.params, { syncOnly: true });
}
// Sync the end mutation to mark the copy worksheet operation is complete
// This will trigger a snapshot save on the server
commandService.syncExecuteCommand(CopyWorksheetEndMutation.id, { unitId, subUnitId: newSheetId }, { syncOnly: true });
// Schedule local execution during idle time
sheetLazyExecuteScheduleService.scheduleMutations(unitId, newSheetId, scheduledMutations);
}
} else {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos,
redoMutations: redos,
});
}
return true;
}
return false;
},
};
// If Sheet1(Copy) already exists and you copy Sheet1, you should get Sheet1(Copy2)
export function getCopyUniqueSheetName(workbook: Workbook, localeService: LocaleService, name: string): string {
let output = `${name} ${localeService.t<LocaleKey>('sheets.tabs.sheetCopy', '')}`;
let count = 2;
while (workbook.checkSheetName(output)) {
output = `${name} ${localeService.t<LocaleKey>('sheets.tabs.sheetCopy', `${count}`)}`;
count++;
}
return output;
}

View File

@@ -0,0 +1,109 @@
/**
* 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, ICommand, IMutationInfo, IRange } from '@univerjs/core';
import type { IDeleteRangeMutationParams } from '../../basics/interfaces/mutation-interface';
import {
CommandType,
Dimension,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { getRemoveRangeMutations } from '../utils/handle-range.mutation';
import { followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IDeleteRangeMoveLeftCommandParams {
range: IRange;
}
export const DeleteRangeMoveLeftCommandId = 'sheet.command.delete-range-move-left';
/**
* The command to delete range.
*/
export const DeleteRangeMoveLeftCommand: ICommand = {
type: CommandType.COMMAND,
id: DeleteRangeMoveLeftCommandId,
handler: async (accessor: IAccessor, params?: IDeleteRangeMoveLeftCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, workbook, subUnitId, unitId } = target;
let range = params?.range;
if (!range) {
range = selectionManagerService.getCurrentLastSelection()?.range;
}
if (!range) return false;
const deleteRangeMutationParams: IDeleteRangeMutationParams = {
range,
subUnitId,
unitId,
shiftDimension: Dimension.COLUMNS,
};
const sheetInterceptor = sheetInterceptorService.onCommandExecute({
id: DeleteRangeMoveLeftCommand.id,
params: { range } as IDeleteRangeMoveLeftCommandParams,
});
const { redo: removeRangeRedo, undo: removeRangeUndo } = getRemoveRangeMutations(
accessor,
deleteRangeMutationParams
);
const redos: IMutationInfo[] = [...(sheetInterceptor.preRedos ?? []), ...removeRangeRedo];
const undos: IMutationInfo[] = [...sheetInterceptor.undos, ...removeRangeUndo];
redos.push(...sheetInterceptor.redos);
redos.push(followSelectionOperation(range, workbook, worksheet));
undos.push(...(sheetInterceptor.preUndos ?? []));
// execute do mutations and add undo mutations to undo stack if completed
const result = sequenceExecute(redos, commandService).result;
if (result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: DeleteRangeMoveLeftCommand.id,
params: { range } as IDeleteRangeMoveLeftCommandParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
undos.push(...afterInterceptors.undos);
redos.push(...afterInterceptors.redos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos.reverse(),
redoMutations: redos,
});
return true;
}
return false;
},
// all subsequent mutations should succeed inorder to make the whole process succeed
// Promise.all([]).then(() => true),
};

View File

@@ -0,0 +1,109 @@
/**
* 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, ICommand, IMutationInfo, IRange } from '@univerjs/core';
import type { IDeleteRangeMutationParams } from '../../basics/interfaces/mutation-interface';
import {
CommandType,
Dimension,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { getRemoveRangeMutations } from '../utils/handle-range.mutation';
import { followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IDeleteRangeMoveUpCommandParams {
range: IRange;
}
export const DeleteRangeMoveUpCommandId = 'sheet.command.delete-range-move-up';
/**
* The command to delete range.
*/
export const DeleteRangeMoveUpCommand: ICommand = {
type: CommandType.COMMAND,
id: DeleteRangeMoveUpCommandId,
handler: async (accessor: IAccessor, params: IDeleteRangeMoveUpCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId, workbook, worksheet } = target;
let range: IRange | undefined = params?.range;
if (!range) {
const currentSelection = selectionManagerService.getCurrentLastSelection();
range = currentSelection?.range as IRange | undefined;
}
if (!range) return false;
const deleteRangeMutationParams: IDeleteRangeMutationParams = {
range,
subUnitId,
unitId,
shiftDimension: Dimension.ROWS,
};
const sheetInterceptor = sheetInterceptorService.onCommandExecute({
id: DeleteRangeMoveUpCommand.id,
params: { range } as IDeleteRangeMoveUpCommandParams,
});
const { redo: removeRangeRedo, undo: removeRangeUndo } = getRemoveRangeMutations(
accessor,
deleteRangeMutationParams
);
const redos: IMutationInfo[] = [...(sheetInterceptor.preRedos ?? []), ...removeRangeRedo];
const undos: IMutationInfo[] = [...sheetInterceptor.undos, ...removeRangeUndo];
redos.push(...sheetInterceptor.redos);
redos.push(followSelectionOperation(range, workbook, worksheet));
undos.push(...(sheetInterceptor.preUndos ?? []));
const result = sequenceExecute(redos, commandService).result;
if (result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: DeleteRangeMoveUpCommand.id,
params: { range } as IDeleteRangeMoveUpCommandParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
undos.push(...afterInterceptors.undos);
redos.push(...afterInterceptors.redos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos.reverse(),
redoMutations: redos,
});
return true;
}
return false;
},
// all subsequent mutations should succeed inorder to make the whole process succeed
// Promise.all([]).then(() => true),
};

View File

@@ -0,0 +1,56 @@
/**
* 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 { ICommand } from '@univerjs/core';
import type { IRangeProtectionRule } from '../../models/range-protection-rule.model';
import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core';
import { AddRangeProtectionMutation } from '../mutations/add-range-protection.mutation';
import { DeleteRangeProtectionMutation } from '../mutations/delete-range-protection.mutation';
export interface IDeleteRangeProtectionCommandParams {
unitId: string;
subUnitId: string;
rule: IRangeProtectionRule;
}
export const DeleteRangeProtectionCommand: ICommand<IDeleteRangeProtectionCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.delete-range-protection',
async handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId, subUnitId, rule } = params;
const redoMutationParam = {
unitId,
subUnitId,
ruleIds: [rule.id],
};
const result = await commandService.executeCommand(DeleteRangeProtectionMutation.id, redoMutationParam);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations: [{ id: DeleteRangeProtectionMutation.id, params: redoMutationParam }],
undoMutations: [{ id: AddRangeProtectionMutation.id, params: { unitId, subUnitId, rules: [rule] } }],
});
}
return true;
},
};

View File

@@ -0,0 +1,55 @@
/**
* 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 { ICommand } from '@univerjs/core';
import type { IWorksheetProtectionRule } from '../../services/permission/type';
import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core';
import { AddWorksheetProtectionMutation } from '../mutations/add-worksheet-protection.mutation';
import { DeleteWorksheetProtectionMutation } from '../mutations/delete-worksheet-protection.mutation';
export interface IDeleteWorksheetProtectionParams {
unitId: string;
subUnitId: string;
rule: IWorksheetProtectionRule;
}
export const DeleteWorksheetProtectionCommand: ICommand<IDeleteWorksheetProtectionParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.delete-worksheet-protection',
handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { rule, unitId, subUnitId } = params;
commandService.executeCommand(DeleteWorksheetProtectionMutation.id, {
unitId,
subUnitId,
});
const redoMutations = [{ id: DeleteWorksheetProtectionMutation.id, params: { unitId, subUnitId } }];
const undoMutations = [{ id: AddWorksheetProtectionMutation.id, params: { unitId, rule, subUnitId } }];
undoRedoService.pushUndoRedo({
unitID: unitId,
redoMutations,
undoMutations,
});
return true;
},
};

View File

@@ -0,0 +1,55 @@
/**
* 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, ICommand } from '@univerjs/core';
import type { IWorksheetRangeThemeStyleMutationParams } from '../../basics/interfaces/mutation-interface';
import {
CommandType,
ICommandService,
IUndoRedoService,
} from '@univerjs/core';
import { SetWorksheetRangeThemeStyleMutation } from '../mutations/add-worksheet-range-theme.mutation';
import {
DeleteWorksheetRangeThemeStyleMutation,
DeleteWorksheetRangeThemeStyleMutationFactory,
} from '../mutations/delete-worksheet-range-theme.mutation';
export const DeleteWorksheetRangeThemeStyleCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-worksheet-range-theme-style',
handler: (accessor: IAccessor, params: IWorksheetRangeThemeStyleMutationParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId } = params;
const undoMutationParams = DeleteWorksheetRangeThemeStyleMutationFactory(accessor, params);
const result = commandService.syncExecuteCommand(DeleteWorksheetRangeThemeStyleMutation.id, params);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: SetWorksheetRangeThemeStyleMutation.id, params: undoMutationParams }],
redoMutations: [{ id: DeleteWorksheetRangeThemeStyleMutation.id, params }],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,58 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import type { ISetDefinedNameMutationParam } from '@univerjs/engine-formula';
import {
CommandType,
ICommandService,
IUndoRedoService,
} from '@univerjs/core';
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '@univerjs/engine-formula';
/**
* The command to insert new defined name
*/
export const InsertDefinedNameCommand: ICommand = {
id: 'sheet.command.insert-defined-name',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: ISetDefinedNameMutationParam) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
if (!params) return false;
// prepare do mutations
const insertSheetMutationParams: ISetDefinedNameMutationParam = {
...params,
};
// execute do mutations and add undo mutations to undo stack if completed
const result = commandService.syncExecuteCommand(SetDefinedNameMutation.id, insertSheetMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: params.unitId,
undoMutations: [{ id: RemoveDefinedNameMutation.id, params: insertSheetMutationParams }],
redoMutations: [{ id: SetDefinedNameMutation.id, params: insertSheetMutationParams }],
});
return true;
}
return false;
},
};

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 { IAccessor, ICellData, ICommand, IMutationInfo, IObjectMatrixPrimitiveType, IRange } from '@univerjs/core';
import type {
IInsertRangeMutationParams,
IInsertRowMutationParams,
IRemoveRowsMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { LocaleKey } from '../../locale/types';
import {
BooleanNumber,
CommandType,
Dimension,
ErrorService,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
Range,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { InsertRowMutation, InsertRowMutationUndoFactory } from '../mutations/insert-row-col.mutation';
import { RemoveRowMutation } from '../mutations/remove-row-col.mutation';
import { getInsertRangeMutations } from '../utils/handle-range.mutation';
import { hasOverlappingRanges } from '../utils/selection-command-util';
import { followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IInsertRangeMoveDownCommandParams {
range: IRange;
}
export const InsertRangeMoveDownCommandId = 'sheet.command.insert-range-move-down';
/**
* The command to insert range.
*/
export const InsertRangeMoveDownCommand: ICommand = {
type: CommandType.COMMAND,
id: InsertRangeMoveDownCommandId,
// eslint-disable-next-line max-lines-per-function
handler: async (accessor: IAccessor, params?: IInsertRangeMoveDownCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const errorService = accessor.get(ErrorService);
const localeService = accessor.get(LocaleService);
if (hasOverlappingRanges(selectionManagerService.getCurrentSelections().map(({ range }) => range))) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.overlappingSelections'));
return false;
}
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { unitId, subUnitId, worksheet, workbook } = target;
let range = params?.range;
if (!range) {
range = selectionManagerService.getCurrentLastSelection()?.range;
}
if (!range) return false;
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
const cellMatrix = worksheet.getCellMatrix();
const dataRange = cellMatrix.getDataRange();
const moveSlice = cellMatrix.getSlice(dataRange.startRow, dataRange.endRow, range.startColumn, range.endColumn);
const sliceMaxRow = moveSlice.getDataRange().endRow;
const insertRowCount = Math.max(sliceMaxRow + (range.endRow - range.startRow + 1) - dataRange.endRow, 0);
if (insertRowCount > 0) {
const anchorRow = range.startRow - 1;
const height = worksheet.getRowHeight(anchorRow);
const insertRowParams: IInsertRowMutationParams = {
unitId,
subUnitId,
range: {
startRow: dataRange.endRow + 1,
endRow: dataRange.endRow + insertRowCount,
startColumn: dataRange.startColumn,
endColumn: dataRange.endColumn,
},
rowInfo: new Array(insertRowCount).fill(undefined).map(() => ({
h: height,
hd: BooleanNumber.FALSE,
})),
};
redoMutations.push({
id: InsertRowMutation.id,
params: insertRowParams,
});
const undoRowInsertionParams: IRemoveRowsMutationParams = InsertRowMutationUndoFactory(
accessor,
insertRowParams
);
undoMutations.push({ id: RemoveRowMutation.id, params: undoRowInsertionParams });
}
// to keep style.
const cellValue: IObjectMatrixPrimitiveType<ICellData> = {};
Range.foreach(range, (row, col) => {
const cell = worksheet.getCell(row, col);
if (!cell) {
return;
}
if (!cellValue[row]) {
cellValue[row] = {};
}
cellValue[row][col] = { s: cell.s };
});
const insertRangeMutationParams: IInsertRangeMutationParams = {
range,
subUnitId,
unitId,
shiftDimension: Dimension.ROWS,
cellValue,
};
const { redo: insertRangeRedo, undo: insertRangeUndo } = getInsertRangeMutations(
accessor,
insertRangeMutationParams
);
redoMutations.push(...insertRangeRedo);
undoMutations.push(...insertRangeUndo);
const sheetInterceptor = sheetInterceptorService.onCommandExecute({
id: InsertRangeMoveDownCommand.id,
params: { range } as IInsertRangeMoveDownCommandParams,
});
redoMutations.push(...sheetInterceptor.redos);
redoMutations.push(followSelectionOperation(range, workbook, worksheet));
undoMutations.push(...(sheetInterceptor.preUndos ?? []));
redoMutations.unshift(...(sheetInterceptor.preRedos ?? []));
undoMutations.unshift(...sheetInterceptor.undos);
// execute do mutations and add undo mutations to undo stack if completed
const result = sequenceExecute(redoMutations, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: InsertRangeMoveDownCommand.id,
params: { range } as IInsertRangeMoveDownCommandParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
undoMutations.push(...afterInterceptors.undos);
redoMutations.push(...afterInterceptors.redos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undoMutations.reverse(),
redoMutations,
});
return true;
}
return false;
},
// all subsequent mutations should succeed inorder to make the whole process succeed
// Promise.all([]).then(() => true),
};

View File

@@ -0,0 +1,189 @@
/**
* 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, ICommand, IMutationInfo, IObjectMatrixPrimitiveType, IRange } from '@univerjs/core';
import type {
IInsertColMutationParams,
IInsertRangeMutationParams,
IRemoveColMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { LocaleKey } from '../../locale/types';
import {
BooleanNumber,
CommandType,
Dimension,
ErrorService,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
Range,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { InsertColMutation, InsertColMutationUndoFactory } from '../mutations/insert-row-col.mutation';
import { RemoveColMutation } from '../mutations/remove-row-col.mutation';
import { getInsertRangeMutations } from '../utils/handle-range.mutation';
import { hasOverlappingRanges } from '../utils/selection-command-util';
import { followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IInsertRangeMoveRightCommandParams {
range: IRange;
}
export const InsertRangeMoveRightCommandId = 'sheet.command.insert-range-move-right';
/**
* The command to insert range.
*/
export const InsertRangeMoveRightCommand: ICommand = {
type: CommandType.COMMAND,
id: InsertRangeMoveRightCommandId,
// eslint-disable-next-line max-lines-per-function
handler: async (accessor: IAccessor, params?: IInsertRangeMoveRightCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const errorService = accessor.get(ErrorService);
const localeService = accessor.get(LocaleService);
if (hasOverlappingRanges(selectionManagerService.getCurrentSelections().map(({ range }) => range))) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.overlappingSelections'));
return false;
}
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { workbook, worksheet, unitId, subUnitId } = target;
let range = params?.range;
if (!range) {
range = selectionManagerService.getCurrentLastSelection()?.range;
}
if (!range) return false;
const redoMutations: IMutationInfo[] = [];
const undoMutations: IMutationInfo[] = [];
const cellMatrix = worksheet.getCellMatrix();
const dataRange = cellMatrix.getDataRange();
const moveSlice = cellMatrix.getSlice(range.startRow, range.endRow, dataRange.startColumn, dataRange.endColumn);
const sliceMaxCol = moveSlice.getDataRange().endColumn;
const insertColCount = Math.max(
sliceMaxCol + (range.endColumn - range.startColumn + 1) - dataRange.endColumn,
0
);
if (insertColCount > 0) {
const anchorCol = range.startColumn - 1;
const width = worksheet.getColumnWidth(anchorCol);
const insertColParams: IInsertColMutationParams = {
unitId,
subUnitId,
range: {
startRow: dataRange.startRow + 1,
endRow: dataRange.endRow,
startColumn: dataRange.endColumn + 1,
endColumn: dataRange.endColumn + insertColCount,
},
colInfo: new Array(insertColCount).fill(undefined).map(() => ({
w: width,
hd: BooleanNumber.FALSE,
})),
};
redoMutations.push({
id: InsertColMutation.id,
params: insertColParams,
});
const undoColInsertionParams: IRemoveColMutationParams = InsertColMutationUndoFactory(
accessor,
insertColParams
);
undoMutations.push({ id: RemoveColMutation.id, params: undoColInsertionParams });
}
// to keep style.
const cellValue: IObjectMatrixPrimitiveType<ICellData> = {};
Range.foreach(range, (row, col) => {
const cell = worksheet.getCell(row, col);
if (!cell || !cell.s) {
return;
}
if (!cellValue[row]) {
cellValue[row] = {};
}
cellValue[row][col] = { s: cell.s };
});
const insertRangeMutationParams: IInsertRangeMutationParams = {
range,
subUnitId,
unitId,
shiftDimension: Dimension.COLUMNS,
cellValue,
};
const { redo: insertRangeRedo, undo: insertRangeUndo } = getInsertRangeMutations(
accessor,
insertRangeMutationParams
);
redoMutations.push(...insertRangeRedo);
undoMutations.push(...insertRangeUndo);
const sheetInterceptor = sheetInterceptorService.onCommandExecute({
id: InsertRangeMoveRightCommand.id,
params: { range } as IInsertRangeMoveRightCommandParams,
});
redoMutations.push(...sheetInterceptor.redos);
redoMutations.push(followSelectionOperation(range, workbook, worksheet));
undoMutations.push(...(sheetInterceptor.preUndos ?? []));
redoMutations.unshift(...(sheetInterceptor.preRedos ?? []));
undoMutations.unshift(...sheetInterceptor.undos);
// execute do mutations and add undo mutations to undo stack if completed
const result = sequenceExecute(redoMutations, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: InsertRangeMoveRightCommand.id,
params: { range } as IInsertRangeMoveRightCommandParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
undoMutations.push(...afterInterceptors.undos);
redoMutations.push(...afterInterceptors.redos);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undoMutations.reverse(),
redoMutations,
});
return true;
}
return false;
},
// all subsequent mutations should succeed in order to make the whole process succeed
// Promise.all([]).then(() => true),
};

View File

@@ -0,0 +1,692 @@
/**
* 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, ICommand, IMutationInfo, IObjectMatrixPrimitiveType, IRange } from '@univerjs/core';
import type {
IInsertColMutationParams,
IInsertRowMutationParams,
IRemoveColMutationParams,
IRemoveRowsMutationParams,
} from '../../basics/interfaces/mutation-interface';
import {
BooleanNumber,
CommandType,
Direction,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
RANGE_TYPE,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import {
InsertColMutation,
InsertColMutationUndoFactory,
InsertRowMutation,
InsertRowMutationUndoFactory,
} from '../mutations/insert-row-col.mutation';
import { RemoveColMutation, RemoveRowMutation } from '../mutations/remove-row-col.mutation';
import { SetRangeValuesMutation } from '../mutations/set-range-values.mutation';
import { copyRangeStyles, followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IInsertRowCommandParams {
unitId: string;
subUnitId: string;
/**
* whether it is inserting row after (DOWN) or inserting before (UP)
*
* this determines styles of the cells in the inserted rows
*/
direction: Direction.UP | Direction.DOWN;
/**
* The range will the row be inserted.
*/
range: IRange;
cellValue?: IObjectMatrixPrimitiveType<ICellData>;
}
export const InsertRowCommandId = 'sheet.command.insert-row';
/**
* this command and its interface should not be exported from index.ts
*
* @internal
*/
export const InsertRowCommand: ICommand = {
type: CommandType.COMMAND,
id: InsertRowCommandId,
handler: async (accessor: IAccessor, params: IInsertRowCommandParams) => {
const commandService = accessor.get(ICommandService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const { range, direction, unitId, subUnitId, cellValue } = params;
const canPerform = await sheetInterceptorService.beforeCommandExecute({
id: InsertRowCommand.id,
params,
});
if (!canPerform) {
return false;
}
return commandService.syncExecuteCommand(InsertRowByRangeCommand.id, {
range,
direction,
unitId,
subUnitId,
cellValue,
});
},
};
export const InsertRowByRangeCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-row-by-range',
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params: IInsertRowCommandParams) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) {
throw new Error('Workbook or Worksheet not found at InsertRowByRangeCommand');
};
const { workbook, worksheet, unitId, subUnitId } = target;
const { range, direction, cellValue } = params;
const { startRow, endRow } = range;
range.rangeType = RANGE_TYPE.ROW;
const anchorRow = direction === Direction.UP ? startRow : startRow - 1;
if (anchorRow < 0 || anchorRow > worksheet.getRowCount() - 1) {
throw new Error('Anchor row is out of bounds in InsertRowByRangeCommand');
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
// insert row properties & undos
const insertRowParams: IInsertRowMutationParams = {
unitId,
subUnitId,
range,
};
const height = worksheet.getRowHeight(anchorRow);
if (height !== worksheet.getConfig().defaultRowHeight) {
insertRowParams.rowInfo = new Array(endRow - startRow + 1).fill(undefined).map(() => ({
h: height,
hd: BooleanNumber.FALSE,
}));
}
const undoRowInsertionParams: IRemoveRowsMutationParams = InsertRowMutationUndoFactory(
accessor,
insertRowParams
);
const redos: IMutationInfo[] = [{ id: InsertRowMutation.id, params: insertRowParams }];
const undos: IMutationInfo[] = [{ id: RemoveRowMutation.id, params: undoRowInsertionParams }];
// set range values
if (cellValue && Object.keys(cellValue).length > 0) {
redos.push({
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue,
},
});
}
const intercepted = sheetInterceptorService.onCommandExecute({
id: InsertRowCommand.id,
params,
});
redos.unshift(...(intercepted.preRedos ?? []));
redos.push(...(intercepted.redos ?? []));
redos.push(followSelectionOperation(range, workbook, worksheet));
undos.unshift(...(intercepted.preUndos ?? []));
undos.push(...(intercepted.undos ?? []));
const result = sequenceExecute(redos, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: InsertRowCommand.id,
params,
});
sequenceExecute(afterInterceptors.redos, commandService);
redos.push(...afterInterceptors.redos);
undos.push(...afterInterceptors.undos);
undoRedoService.pushUndoRedo({
unitID: params.unitId,
undoMutations: undos,
redoMutations: redos,
});
return true;
}
return false;
},
};
export const InsertRowBeforeCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-row-before',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
let range: IRange;
if (selections?.length === 1) {
range = selections[0];
} else {
// if there are multi selections, we can't decide which row to insert
// in fact, UI would hides / disables the insert row button
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, subUnitId, unitId } = target;
const count = params.value || 0;
const startRow = range.startRow;
const endRow = range.startRow + count - 1;
const startColumn = 0;
const endColumn = worksheet.getColumnCount() - 1;
const insertRowParams: IInsertRowCommandParams = {
unitId,
subUnitId,
direction: Direction.UP,
range: {
startRow,
endRow,
startColumn,
endColumn,
},
// copy styles from the row above
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, true, startRow - 1),
};
return accessor.get(ICommandService).executeCommand(InsertRowCommand.id, insertRowParams);
},
};
export const InsertRowAfterCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-row-after',
handler: async (accessor: IAccessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
let range: IRange;
if (selections?.length === 1) {
range = selections[0];
} else {
// if there are multi selections, we can't decide which row to insert
// in fact, UI would hides / disables the insert row button
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = range.endRow - range.startRow + 1;
const startRow = range.endRow + 1;
const endRow = range.endRow + count;
const startColumn = 0;
const endColumn = worksheet.getColumnCount() - 1;
const insertRowParams: IInsertRowCommandParams = {
unitId,
subUnitId,
direction: Direction.DOWN,
range: {
startRow,
endRow,
startColumn,
endColumn,
rangeType: RANGE_TYPE.ROW,
},
// copy styles from the row below
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, true, range.endRow),
};
return accessor.get(ICommandService).executeCommand(InsertRowCommand.id, insertRowParams);
},
};
export interface IInsertMultiRowsCommandParams {
value: number;
}
export const InsertMultiRowsAboveCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-multi-rows-above',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
let range: IRange;
if (selections?.length === 1) {
range = selections[0];
} else {
// if there are multi selections, we can't decide which row to insert
// in fact, UI would hides / disables the insert row button
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = params.value || 0;
const startRow = range.startRow;
const endRow = range.startRow + count - 1;
const startColumn = 0;
const endColumn = worksheet.getColumnCount() - 1;
const copiedStyle = copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, true, startRow - 1);
const insertRowParams: IInsertRowCommandParams = {
unitId,
subUnitId,
direction: Direction.UP,
range: {
startRow,
endRow,
startColumn,
endColumn,
rangeType: RANGE_TYPE.ROW,
},
// copy styles from the row above
cellValue: copiedStyle,
};
return accessor.get(ICommandService).executeCommand(InsertRowCommand.id, insertRowParams);
},
};
export const InsertMultiRowsAfterCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-multi-rows-after',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
let range: IRange;
if (selections?.length === 1) {
range = selections[0];
} else {
// if there are multi selections, we can't decide which row to insert
// in fact, UI would hides / disables the insert row button
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = params.value || 0;
const startRow = range.endRow + 1;
const endRow = range.endRow + count;
const startColumn = 0;
const endColumn = worksheet.getColumnCount() - 1;
const insertRowParams: IInsertRowCommandParams = {
unitId,
subUnitId,
direction: Direction.DOWN,
range: {
startRow,
endRow,
startColumn,
endColumn,
rangeType: RANGE_TYPE.ROW,
},
// copy styles from the row below
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, true, range.endRow),
};
return accessor.get(ICommandService).executeCommand(InsertRowCommand.id, insertRowParams);
},
};
export interface IInsertColCommandParams {
unitId: string;
subUnitId: string;
range: IRange;
direction: Direction.LEFT | Direction.RIGHT;
cellValue?: IObjectMatrixPrimitiveType<ICellData>;
}
export const InsertColCommandId = 'sheet.command.insert-col';
export const InsertColCommand: ICommand<IInsertColCommandParams> = {
type: CommandType.COMMAND,
id: InsertColCommandId,
handler: async (accessor: IAccessor, params: IInsertColCommandParams) => {
const commandService = accessor.get(ICommandService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const { range, direction, subUnitId, unitId, cellValue } = params;
const canPerform = await sheetInterceptorService.beforeCommandExecute({
id: InsertColCommand.id,
params,
});
if (!canPerform) {
return false;
}
return commandService.syncExecuteCommand(InsertColByRangeCommand.id, {
range,
direction,
unitId,
subUnitId,
cellValue,
});
},
};
export const InsertColByRangeCommand: ICommand<IInsertColCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-col-by-range',
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params: IInsertColCommandParams) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) {
throw new Error('Workbook or Worksheet not found at InsertColByRangeCommand');
}
const { workbook, worksheet, unitId, subUnitId } = target;
const { range, direction, cellValue } = params;
const { startColumn, endColumn } = range;
range.rangeType = RANGE_TYPE.COLUMN;
const anchorCol = direction === Direction.LEFT ? startColumn : startColumn - 1;
if (anchorCol < 0 || anchorCol > worksheet.getColumnCount() - 1) {
throw new Error('Anchor column is out of bounds in InsertColByRangeCommand');
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
// insert cols & undos
const insertColParams: IInsertColMutationParams = {
unitId,
subUnitId,
range,
};
const width = worksheet.getColumnWidth(anchorCol);
if (width !== worksheet.getConfig().defaultColumnWidth) {
insertColParams.colInfo = new Array(endColumn - startColumn + 1).fill(undefined).map(() => ({
w: width,
hd: BooleanNumber.FALSE,
}));
}
const undoColInsertionParams: IRemoveColMutationParams = InsertColMutationUndoFactory(
accessor,
insertColParams
);
const redos: IMutationInfo[] = [{ id: InsertColMutation.id, params: insertColParams }];
const undos: IMutationInfo[] = [{ id: RemoveColMutation.id, params: undoColInsertionParams }];
// set range values
if (cellValue && Object.keys(cellValue).length > 0) {
redos.push({
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue,
},
});
}
const intercepted = sheetInterceptorService.onCommandExecute({
id: InsertColCommand.id,
params,
});
redos.unshift(...(intercepted.preRedos ?? []));
redos.push(...(intercepted.redos ?? []));
redos.push(followSelectionOperation(range, workbook, worksheet));
undos.unshift(...(intercepted.preUndos ?? []));
undos.push(...(intercepted.undos ?? []));
const result = sequenceExecute(redos, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: InsertColCommand.id,
params,
});
sequenceExecute(afterInterceptors.redos, commandService);
redos.push(...afterInterceptors.redos);
undos.push(...afterInterceptors.undos);
undoRedoService.pushUndoRedo({
unitID: params.unitId,
undoMutations: undos.filter(Boolean),
redoMutations: redos.filter(Boolean),
});
return true;
}
return false;
},
};
export const InsertColBeforeCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-col-before',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections();
let range: IRange;
if (selections?.length === 1) {
range = selections[0].range;
} else {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = params.value || 0;
const startColumn = range.startColumn;
const endColumn = range.startColumn + count - 1;
const startRow = 0;
const endRow = worksheet.getRowCount() - 1;
const insertColParams: IInsertColCommandParams = {
unitId,
subUnitId,
direction: Direction.LEFT,
range: {
startColumn,
endColumn,
startRow,
endRow,
rangeType: RANGE_TYPE.COLUMN,
},
// copy styles from the column before
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, false, startColumn - 1),
};
return accessor.get(ICommandService).executeCommand(InsertColCommand.id, insertColParams);
},
};
export const InsertColAfterCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-col-after',
handler: async (accessor: IAccessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections();
let range: IRange;
if (selections?.length === 1) {
range = selections[0].range;
} else {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = range.endColumn - range.startColumn + 1;
const startColumn = range.endColumn + 1;
const endColumn = range.endColumn + count;
const startRow = 0;
const endRow = worksheet.getRowCount() - 1;
const insertColParams: IInsertColCommandParams = {
unitId,
subUnitId,
direction: Direction.RIGHT,
range: {
startColumn,
endColumn,
startRow,
endRow,
},
// copy styles from the column after
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, false, range.endColumn),
};
return accessor.get(ICommandService).executeCommand(InsertColCommand.id, insertColParams);
},
};
export interface IInsertMultiColsCommandParams {
value: number;
}
export const InsertMultiColsLeftCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-multi-cols-before',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections();
let range: IRange;
if (selections?.length === 1) {
range = selections[0].range;
} else {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = params.value || 0;
const startColumn = range.startColumn;
const endColumn = range.startColumn + count - 1;
const startRow = 0;
const endRow = worksheet.getRowCount() - 1;
const insertColParams: IInsertColCommandParams = {
unitId,
subUnitId,
direction: Direction.LEFT,
range: {
startColumn,
endColumn,
startRow,
endRow,
rangeType: RANGE_TYPE.COLUMN,
},
// copy styles from the column before
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, false, startColumn - 1),
};
return accessor.get(ICommandService).executeCommand(InsertColCommand.id, insertColParams);
},
};
export const InsertMultiColsRightCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.insert-multi-cols-right',
handler: async (accessor: IAccessor, params: IInsertMultiRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const selections = selectionManagerService.getCurrentSelections();
let range: IRange;
if (selections?.length === 1) {
range = selections[0].range;
} else {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
const count = params.value || 0;
const startColumn = range.endColumn + 1;
const endColumn = range.endColumn + count;
const startRow = 0;
const endRow = worksheet.getRowCount() - 1;
const insertColParams: IInsertColCommandParams = {
unitId,
subUnitId,
direction: Direction.RIGHT,
range: {
startColumn,
endColumn,
startRow,
endRow,
},
// copy styles from the column after
cellValue: copyRangeStyles(worksheet, startRow, endRow, startColumn, endColumn, false, range.endColumn),
};
return accessor.get(ICommandService).executeCommand(InsertColCommand.id, insertColParams);
},
};

View File

@@ -0,0 +1,100 @@
/**
* 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, ICommand, IWorksheetData } from '@univerjs/core';
import type {
IInsertSheetMutationParams,
IRemoveSheetMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { LocaleKey } from '../../locale/types';
import {
CommandType,
generateRandomId,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
mergeWorksheetSnapshotWithDefault,
} from '@univerjs/core';
import { InsertSheetMutation, InsertSheetUndoMutationFactory } from '../mutations/insert-sheet.mutation';
import { RemoveSheetMutation } from '../mutations/remove-sheet.mutation';
import { getSheetCommandTargetWorkbook } from './utils/target-util';
export interface IInsertSheetCommandParams {
unitId?: string;
index?: number;
sheet?: IWorksheetData;
}
/**
* The command to insert new worksheet
*/
export const InsertSheetCommand: ICommand = {
id: 'sheet.command.insert-sheet',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IInsertSheetCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const localeService = accessor.get(LocaleService);
const target = getSheetCommandTargetWorkbook(univerInstanceService, { unitId: params?.unitId });
if (!target) return false;
const { unitId, workbook } = target;
let index = workbook.getSheets().length;
const sheet = params?.sheet;
const sheetId = sheet?.id;
const sheetName = sheet?.name;
const sheetConfig = mergeWorksheetSnapshotWithDefault(sheet || {});
if (params) {
index = params.index ?? index;
sheetConfig.id = sheetId || generateRandomId();
sheetConfig.name = sheetName
? workbook.uniqueSheetName(sheetName)
: workbook.generateNewSheetName(`${localeService.t<LocaleKey>('sheets.tabs.sheet')}`);
} else {
sheetConfig.id = generateRandomId();
sheetConfig.name = workbook.generateNewSheetName(`${localeService.t<LocaleKey>('sheets.tabs.sheet')}`);
}
// prepare do mutations
const insertSheetMutationParams: IInsertSheetMutationParams = {
index,
sheet: sheetConfig,
unitId,
};
const removeSheetMutationParams: IRemoveSheetMutationParams = InsertSheetUndoMutationFactory(
accessor,
insertSheetMutationParams
);
// execute do mutations and add undo mutations to undo stack if completed
const result = commandService.syncExecuteCommand(InsertSheetMutation.id, insertSheetMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: RemoveSheetMutation.id, params: removeSheetMutationParams }],
redoMutations: [{ id: InsertSheetMutation.id, params: insertSheetMutationParams }],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,349 @@
/**
* 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,
ICommand,
IRange,
ISelectionCell,
Nullable,
Workbook,
Worksheet,
} from '@univerjs/core';
import type { LocaleKey } from '../../locale/types';
import type { IMoveRangeMutationParams } from '../mutations/move-range.mutation';
import type { ISetSelectionsOperationParams } from '../operations/selection.operation';
import {
cellToRange,
CommandType,
ErrorService,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
ObjectMatrix,
Range,
Rectangle,
sequenceExecute,
Tools,
UniverInstanceType,
} from '@univerjs/core';
import { SelectionMoveType } from '../../services/selections/type';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { MoveRangeMutation } from '../mutations/move-range.mutation';
import { SetSelectionsOperation } from '../operations/selection.operation';
import { alignToMergedCellsBorders, getPrimaryForRange } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IMoveRangeCommandParams {
toRange: IRange;
fromRange: IRange;
fromUnitId?: string;
fromSubUnitId?: string;
toUnitId?: string;
toSubUnitId?: string;
}
export const MoveRangeCommandId = 'sheet.command.move-range';
interface IResolvedMoveRangeContext {
unitId: string;
fromSubUnitId: string;
toSubUnitId: string;
fromWorksheet: Worksheet;
toWorksheet: Worksheet;
}
interface IGetMoveRangeCommandMutationsOptions {
includeSelection?: boolean;
includeAfterCommand?: boolean;
includeAutoHeight?: boolean;
}
export const MoveRangeCommand: ICommand = {
type: CommandType.COMMAND,
id: MoveRangeCommandId,
handler: async (accessor: IAccessor, params: IMoveRangeCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const errorService = accessor.get(ErrorService);
const localeService = accessor.get(LocaleService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const perform = await sheetInterceptorService.beforeCommandExecute({ id: MoveRangeCommand.id, params });
if (!perform) {
return false;
}
const moveRangeCommandMutations = getMoveRangeCommandMutations(accessor, params);
if (!moveRangeCommandMutations) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.acrossMergedCell'));
return false;
}
const result = sequenceExecute(moveRangeCommandMutations.redos, commandService).result;
if (result) {
undoRedoService.pushUndoRedo({
unitID: moveRangeCommandMutations.unitId,
undoMutations: moveRangeCommandMutations.undos,
redoMutations: moveRangeCommandMutations.redos,
});
return true;
}
return false;
},
};
function _resolveMoveRangeContext(accessor: IAccessor, params: IMoveRangeCommandParams): IResolvedMoveRangeContext | null {
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
const unitId = params.toUnitId ?? params.fromUnitId ?? target?.unitId;
const fromSubUnitId = params.fromSubUnitId ?? target?.subUnitId;
const toSubUnitId = params.toSubUnitId ?? params.fromSubUnitId ?? target?.subUnitId;
if (!unitId || !fromSubUnitId || !toSubUnitId) {
return null;
}
if (params.fromUnitId && params.toUnitId && params.fromUnitId !== params.toUnitId) {
return null;
}
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
const fromWorksheet = workbook?.getSheetBySheetId(fromSubUnitId);
const toWorksheet = workbook?.getSheetBySheetId(toSubUnitId);
if (!fromWorksheet || !toWorksheet) {
return null;
}
return {
unitId,
fromSubUnitId,
toSubUnitId,
fromWorksheet,
toWorksheet,
};
}
export function getMoveRangeCommandMutations(
accessor: IAccessor,
params: IMoveRangeCommandParams,
options: IGetMoveRangeCommandMutationsOptions = {}
) {
const {
includeSelection = true,
includeAfterCommand = true,
includeAutoHeight = true,
} = options;
const context = _resolveMoveRangeContext(accessor, params);
if (!context) {
return null;
}
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const { unitId, fromSubUnitId, toSubUnitId, fromWorksheet, toWorksheet } = context;
const moveRangeMutations = getMoveRangeUndoRedoMutations(
accessor,
{ unitId, subUnitId: fromSubUnitId, range: params.fromRange },
{ unitId, subUnitId: toSubUnitId, range: params.toRange }
);
if (moveRangeMutations === null) {
return null;
}
const commandInfo = {
id: MoveRangeCommand.id,
params,
};
const interceptorCommands = sheetInterceptorService.onCommandExecute(commandInfo);
const redos = [
...(interceptorCommands.preRedos ?? []),
...moveRangeMutations.redos,
...interceptorCommands.redos,
];
const undos = [
...(interceptorCommands.preUndos ?? []),
...moveRangeMutations.undos,
...interceptorCommands.undos,
];
if (includeSelection) {
redos.push({
id: SetSelectionsOperation.id,
params: {
unitId,
subUnitId: toSubUnitId,
selections: [{ range: params.toRange, primary: getPrimaryAfterMove(params.fromRange, params.toRange, fromWorksheet, toWorksheet) }],
type: SelectionMoveType.MOVE_END,
} as ISetSelectionsOperationParams,
});
undos.push({
id: SetSelectionsOperation.id,
params: {
unitId,
subUnitId: fromSubUnitId,
selections: [{ range: params.fromRange, primary: getPrimaryForRange(params.fromRange, fromWorksheet) }],
type: SelectionMoveType.MOVE_END,
} as ISetSelectionsOperationParams,
});
}
if (includeAfterCommand) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute(commandInfo);
redos.push(...afterInterceptors.redos);
undos.push(...afterInterceptors.undos);
}
if (includeAutoHeight) {
const { undos: autoHeightUndos, redos: autoHeightRedos } = sheetInterceptorService.generateMutationsOfAutoHeight({
unitId,
subUnitId: toSubUnitId,
ranges: fromSubUnitId === toSubUnitId ? [params.fromRange, params.toRange] : [params.toRange],
});
redos.push(...autoHeightRedos);
undos.push(...autoHeightUndos);
}
return {
unitId,
redos,
undos,
};
}
export interface IRangeUnit {
unitId: string;
subUnitId: string;
range: IRange;
}
// eslint-disable-next-line max-lines-per-function
export function getMoveRangeUndoRedoMutations(
accessor: IAccessor,
from: IRangeUnit,
to: IRangeUnit,
ignoreMerge = false
) {
const unitId = from.unitId;
const workbook = accessor.get(IUniverInstanceService).getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return null;
}
const { subUnitId: fromSubUnitId, range: fromRange } = from;
const { subUnitId: toSubUnitId, range: toRange } = to;
const fromWorksheet = workbook.getSheetBySheetId(fromSubUnitId);
const toWorksheet = workbook.getSheetBySheetId(toSubUnitId);
if (!fromWorksheet || !toWorksheet) {
return null;
}
const alignedRangeWithToRange = alignToMergedCellsBorders(toRange, toWorksheet, false);
if (!Rectangle.equals(toRange, alignedRangeWithToRange) && !ignoreMerge) {
return null;
}
const fromWorksheetCellMatrix = fromWorksheet.getCellMatrix();
const toWorksheetCellMatrix = toWorksheet.getCellMatrix();
const fromRedoCellValue = new ObjectMatrix<Nullable<ICellData>>();
const fromUndoCellValue = new ObjectMatrix<Nullable<ICellData>>();
const toRedoCellValue = new ObjectMatrix<Nullable<ICellData>>();
const toUndoCellValue = new ObjectMatrix<Nullable<ICellData>>();
Range.foreach(fromRange, (row, col) => {
const cellData = Tools.deepClone(fromWorksheetCellMatrix.getValue(row, col)) ?? null;
if (cellData?.s) {
cellData.s = workbook.getStyles().get(cellData.s);
}
fromRedoCellValue.setValue(row, col, null);
fromUndoCellValue.setValue(row, col, cellData);
const cellRange = cellToRange(row, col);
const relativeRange = Rectangle.getRelativeRange(cellRange, fromRange);
const range = Rectangle.getPositionRange(relativeRange, toRange);
toRedoCellValue.setValue(range.startRow, range.startColumn, Tools.deepClone(cellData));
});
Range.foreach(toRange, (row, col) => {
const cellData = Tools.deepClone(toWorksheetCellMatrix.getValue(row, col)) ?? null;
toUndoCellValue.setValue(row, col, cellData);
});
const redoMoveRangeMutationParams: IMoveRangeMutationParams = {
fromRange: from.range,
toRange: to.range,
from: {
value: fromRedoCellValue.getMatrix(),
subUnitId: fromSubUnitId,
},
to: {
value: toRedoCellValue.getMatrix(),
subUnitId: toSubUnitId,
},
unitId,
};
const undoMoveRangeMutationParams: IMoveRangeMutationParams = {
fromRange: to.range,
toRange: from.range,
from: {
value: toUndoCellValue.getMatrix(),
subUnitId: toSubUnitId,
},
to: {
value: fromUndoCellValue.getMatrix(),
subUnitId: fromSubUnitId,
},
unitId,
};
return {
redos: [{ id: MoveRangeMutation.id, params: redoMoveRangeMutationParams }],
undos: [{ id: MoveRangeMutation.id, params: undoMoveRangeMutationParams }],
};
}
// Before moveRange is executed, the target area has no merge cell yet.
// So need to get the merge info of the start cell and then transform it
function getPrimaryAfterMove(fromRange: IRange, toRange: IRange, sourceWorksheet: Worksheet, targetWorksheet: Worksheet = sourceWorksheet): ISelectionCell {
const startRow = fromRange.startRow;
const startColumn = fromRange.startColumn;
const mergeInfo = sourceWorksheet.getMergedCell(startRow, startColumn);
const res = getPrimaryForRange(toRange, targetWorksheet);
if (mergeInfo) {
const mergeRowCount = mergeInfo.endRow - mergeInfo.startRow + 1;
const mergeColCount = mergeInfo.endColumn - mergeInfo.startColumn + 1;
res.endRow = res.startRow + mergeRowCount - 1;
res.endColumn = res.startColumn + mergeColCount - 1;
res.actualRow = res.startRow;
res.actualColumn = res.startColumn;
res.isMerged = false;
res.isMergedMainCell = true;
}
return res;
}

View File

@@ -0,0 +1,366 @@
/**
* 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, ICommand, IRange, Worksheet } from '@univerjs/core';
import type { ISelectionWithStyle } from '../../basics';
import type { LocaleKey } from '../../locale/types';
import type { IMoveColumnsMutationParams, IMoveRowsMutationParams } from '../mutations/move-rows-cols.mutation';
import type { ISetSelectionsOperationParams } from '../operations/selection.operation';
import {
CommandType,
ErrorService,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
LocaleService,
RANGE_TYPE,
Rectangle,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SelectionMoveType } from '../../services/selections/type';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import {
MoveColsMutation,
MoveColsMutationUndoFactory,
MoveRowsMutation,
MoveRowsMutationUndoFactory,
} from '../mutations/move-rows-cols.mutation';
import { SetSelectionsOperation } from '../operations/selection.operation';
import { alignToMergedCellsBorders, getPrimaryForRange } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IMoveRowsCommandParams {
unitId?: string;
subUnitId?: string;
range?: IRange; // for facade to use, accepting user parameters
fromRange: IRange;
toRange: IRange;
}
function rowAcrossMergedCell(row: number, worksheet: Worksheet): boolean {
return worksheet.getMergeData().some((mergedCell) => mergedCell.startRow < row && row <= mergedCell.endRow);
}
function columnAcrossMergedCell(col: number, worksheet: Worksheet): boolean {
return worksheet.getMergeData().some((mergedCell) => mergedCell.startColumn < col && col <= mergedCell.endColumn);
}
export const MoveRowsCommandId = 'sheet.command.move-rows' as const;
/**
* Command to move the selected rows (must currently selected) to the specified row.
*/
export const MoveRowsCommand: ICommand<IMoveRowsCommandParams> = {
id: MoveRowsCommandId,
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params: IMoveRowsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const {
fromRange: { startRow: fromRow },
toRange: { startRow: toRow },
range,
} = params;
// user can specify the range to move, or use the current selection
const selections = range ? [covertRangeToSelection(range)] : selectionManagerService.getCurrentSelections();
const filteredSelections = selections?.filter(
(selection) =>
selection.range.rangeType === RANGE_TYPE.ROW &&
selection.range.startRow <= fromRow &&
fromRow <= selection.range.endRow
);
if (filteredSelections?.length !== 1) {
return false;
}
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { workbook, worksheet } = target;
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const errorService = accessor.get(ErrorService);
const localeService = accessor.get(LocaleService);
// Forbid action when some parts of a merged cell are selected.
const rangeToMove = filteredSelections[0].range;
const beforePrimary = filteredSelections[0].primary;
const alignedRange = alignToMergedCellsBorders(rangeToMove, worksheet, false);
if (!Rectangle.equals(rangeToMove, alignedRange)) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.partOfCell'));
return false;
}
if (rowAcrossMergedCell(toRow, worksheet)) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.acrossMergedCell'));
return false;
}
const destinationRange: IRange = {
...rangeToMove,
startRow: toRow,
endRow: toRow + rangeToMove.endRow - rangeToMove.startRow,
};
const moveRowsParams: IMoveRowsMutationParams = {
unitId,
subUnitId,
sourceRange: rangeToMove,
targetRange: destinationRange,
};
const undoMoveRowsParams = MoveRowsMutationUndoFactory(accessor, moveRowsParams);
const commandService = accessor.get(ICommandService);
const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: MoveRowsCommand.id, params });
const redos = [
...(interceptorCommands.preRedos ?? []),
{ id: MoveRowsMutation.id, params: moveRowsParams },
];
const undos = [
...(interceptorCommands.preUndos ?? []),
{ id: MoveRowsMutation.id, params: undoMoveRowsParams },
];
// Handle selection in user interaction, not when using the Facade API
if (beforePrimary) {
// we could just move the merged cells because other situations are not allowed
// we should only deal with merged cell that is between the range of the rows to move
const movedLength = toRow - fromRow;
const moveBackward = movedLength < 0;
const count = rangeToMove.endRow - rangeToMove.startRow + 1;
// deal with selections
const destSelection: IRange = moveBackward
? destinationRange
: {
...destinationRange,
startRow: destinationRange.startRow - count,
endRow: destinationRange.endRow - count,
};
const setSelectionsParam: ISetSelectionsOperationParams = {
unitId,
subUnitId,
type: SelectionMoveType.MOVE_END,
selections: [{
range: destSelection,
primary: getPrimaryForRange(destSelection, worksheet),
style: null,
}],
};
const undoSetSelectionsParam: ISetSelectionsOperationParams = {
unitId,
subUnitId,
type: SelectionMoveType.MOVE_END,
selections: [{ range: rangeToMove, primary: beforePrimary, style: null }],
};
redos.push({ id: SetSelectionsOperation.id, params: setSelectionsParam });
undos.push({ id: SetSelectionsOperation.id, params: undoSetSelectionsParam });
}
redos.push(...interceptorCommands.redos);
undos.push(...interceptorCommands.undos);
const result = sequenceExecute(redos, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: MoveRowsCommand.id,
params,
});
sequenceExecute(afterInterceptors.redos, commandService);
redos.push(...afterInterceptors.redos);
undos.push(...afterInterceptors.undos);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos,
redoMutations: redos,
});
return true;
}
return false;
},
};
export interface IMoveColsCommandParams {
unitId?: string;
subUnitId?: string;
range?: IRange; // for facade to use, accepting user parameters
fromRange: IRange;
toRange: IRange;
}
export const MoveColsCommandId = 'sheet.command.move-cols' as const;
export const MoveColsCommand: ICommand<IMoveColsCommandParams> = {
id: MoveColsCommandId,
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params: IMoveColsCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const {
fromRange: { startColumn: fromCol },
toRange: { startColumn: toCol },
range,
} = params;
// user can specify the range to move, or use the current selection
const selections = range ? [covertRangeToSelection(range)] : selectionManagerService.getCurrentSelections();
const filteredSelections = selections?.filter(
(selection) =>
selection.range.rangeType === RANGE_TYPE.COLUMN &&
selection.range.startColumn <= fromCol &&
fromCol <= selection.range.endColumn
);
if (filteredSelections?.length !== 1) {
return false;
}
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { workbook, worksheet } = target;
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const errorService = accessor.get(ErrorService);
const localeService = accessor.get(LocaleService);
// Forbid action when some parts of a merged cell are selected.
const rangeToMove = filteredSelections[0].range;
const beforePrimary = filteredSelections[0].primary;
const alignedRange = alignToMergedCellsBorders(rangeToMove, worksheet, false);
if (!Rectangle.equals(rangeToMove, alignedRange)) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.partOfCell'));
return false;
}
if (columnAcrossMergedCell(toCol, worksheet)) {
errorService.emit(localeService.t<LocaleKey>('sheets.info.acrossMergedCell'));
return false;
}
const destinationRange: IRange = {
...rangeToMove,
startColumn: toCol,
endColumn: toCol + rangeToMove.endColumn - rangeToMove.startColumn,
};
const moveColsParams: IMoveColumnsMutationParams = {
unitId,
subUnitId,
sourceRange: rangeToMove,
targetRange: destinationRange,
};
const undoMoveColsParams = MoveColsMutationUndoFactory(accessor, moveColsParams);
const commandService = accessor.get(ICommandService);
const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: MoveColsCommand.id, params });
const redos = [
...(interceptorCommands.preRedos ?? []),
{ id: MoveColsMutation.id, params: moveColsParams },
];
const undos = [
...(interceptorCommands.preUndos ?? []),
{ id: MoveColsMutation.id, params: undoMoveColsParams },
];
// Handle selection in user interaction, not when using the Facade API
if (beforePrimary) {
// we could just move the merged cells because other situations are not allowed
// we should only deal with merged cell that is between the range of the cols to move
const count = rangeToMove.endColumn - rangeToMove.startColumn + 1;
const movedLength = toCol - fromCol;
const moveBackward = movedLength < 0;
// deal with selections
const destSelection: IRange = moveBackward
? destinationRange
: {
...destinationRange,
startColumn: destinationRange.startColumn - count,
endColumn: destinationRange.endColumn - count,
};
const setSelectionsParam: ISetSelectionsOperationParams = {
unitId,
subUnitId,
type: SelectionMoveType.MOVE_END,
selections: [{ range: destSelection, primary: getPrimaryForRange(destSelection, worksheet), style: null }],
};
const undoSetSelectionsParam: ISetSelectionsOperationParams = {
unitId,
subUnitId,
type: SelectionMoveType.MOVE_END,
selections: [{ range: rangeToMove, primary: beforePrimary, style: null }],
};
redos.push({ id: SetSelectionsOperation.id, params: setSelectionsParam });
undos.push({ id: SetSelectionsOperation.id, params: undoSetSelectionsParam });
}
redos.push(...interceptorCommands.redos);
undos.push(...interceptorCommands.undos);
const result = sequenceExecute(redos, commandService);
if (result.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: MoveColsCommand.id,
params,
});
sequenceExecute(afterInterceptors.redos, commandService);
redos.push(...afterInterceptors.redos);
undos.push(...afterInterceptors.undos);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos,
redoMutations: redos,
});
return true;
}
return false;
},
};
function covertRangeToSelection(range: IRange): ISelectionWithStyle {
return {
range,
primary: null,
style: null,
};
}

View File

@@ -0,0 +1,33 @@
/**
* 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, ICommand } from '@univerjs/core';
import type { AUTO_FILL_APPLY_TYPE } from '../../services/auto-fill/type';
import { CommandType } from '@univerjs/core';
import { IAutoFillService } from '../../services/auto-fill/auto-fill.service';
export interface IRefillCommandParams {
type: AUTO_FILL_APPLY_TYPE;
}
export const RefillCommand: ICommand = {
id: 'sheet.command.refill',
type: CommandType.COMMAND,
handler: async (accessor: IAccessor, params: IRefillCommandParams) => {
const autoFillService = accessor.get(IAutoFillService);
return autoFillService.fillData(params.type);
},
};

View File

@@ -0,0 +1,62 @@
/**
* 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 { ICommand } from '@univerjs/core';
import type { RangeThemeStyle } from '../../models/range-theme-util';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService } from '@univerjs/core';
import { RegisterWorksheetRangeThemeStyleMutation } from '../mutations/register-range-theme.mutation';
import { UnregisterWorksheetRangeThemeStyleMutation } from '../mutations/unregister-range-theme-style.mutation';
import { getSheetCommandTarget } from './utils/target-util';
export interface IRegisterWorksheetRangeThemeStyleCommandParams {
unitId: string;
rangeThemeStyle: RangeThemeStyle;
}
export const RegisterWorksheetRangeThemeStyleCommand: ICommand<IRegisterWorksheetRangeThemeStyleCommandParams> = {
id: 'sheet.command.register-worksheet-range-theme-style',
type: CommandType.COMMAND,
handler: (accessor, params) => {
if (!params) {
return false;
}
const { unitId, rangeThemeStyle } = params;
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const redoParam = {
unitId,
themeName: rangeThemeStyle.getName(),
rangeThemeStyleJson: rangeThemeStyle.toJson(),
};
const undoParam = {
unitId,
themeName: rangeThemeStyle.getName(),
};
const result = commandService.syncExecuteCommand(RegisterWorksheetRangeThemeStyleMutation.id, redoParam);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: UnregisterWorksheetRangeThemeStyleMutation.id, params: undoParam }],
redoMutations: [{ id: RegisterWorksheetRangeThemeStyleMutation.id, params: redoParam }],
});
}
return true;
},
};

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 type { IAccessor, ICommand } from '@univerjs/core';
import type { ISetDefinedNameMutationParam } from '@univerjs/engine-formula';
import {
CommandType,
ICommandService,
IUndoRedoService,
sequenceExecute,
} from '@univerjs/core';
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '@univerjs/engine-formula';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
/**
* The command to remove new defined name
*/
export const RemoveDefinedNameCommand: ICommand = {
id: 'sheet.command.remove-defined-name',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: ISetDefinedNameMutationParam) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
if (!params) return false;
const removeSheetMutationParams: ISetDefinedNameMutationParam = {
...params,
};
const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: RemoveDefinedNameCommand.id, params });
const redos = [
...(interceptorCommands.preRedos ?? []),
{ id: RemoveDefinedNameMutation.id, params: removeSheetMutationParams },
...interceptorCommands.redos,
];
const undos = [
...(interceptorCommands.preUndos ?? []),
{ id: SetDefinedNameMutation.id, params: removeSheetMutationParams },
...interceptorCommands.undos,
];
const result = sequenceExecute(redos, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
unitID: params.unitId,
undoMutations: undos.filter(Boolean),
redoMutations: redos.filter(Boolean),
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,344 @@
/**
* 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, ICommand, IMutationInfo, IRange, Workbook, Worksheet } from '@univerjs/core';
import type {
IInsertColMutationParams,
IInsertRowMutationParams,
IRemoveColMutationParams,
IRemoveRowsMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import {
CommandType,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
sequenceExecute,
throttle,
} from '@univerjs/core';
import { getVisibleRanges } from '../../basics/utils';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { InsertColMutation, InsertRowMutation } from '../mutations/insert-row-col.mutation';
import {
RemoveColMutation,
RemoveColMutationFactory,
RemoveRowMutation,
RemoveRowsUndoMutationFactory,
} from '../mutations/remove-row-col.mutation';
import { SetRangeValuesMutation } from '../mutations/set-range-values.mutation';
import { followSelectionOperation } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface IRemoveRowColCommandParams extends Partial<ISheetCommandSharedParams> {
range: IRange;
}
export interface IRemoveRowColCommandInterceptParams extends IRemoveRowColCommandParams {
unitId: string;
subUnitId: string;
ranges?: IRange[];
}
export interface IRemoveRowByRangeCommandParams {
range: IRange;
unitId: string;
subUnitId: string;
}
export interface IRemoveColByRangeCommandParams {
range: IRange;
unitId: string;
subUnitId: string;
}
/**
* Set selection after remove row/col through throttle to avoid frequent set selection operation
*/
const setSelection = throttle((range: IRange, workbook: Workbook, worksheet: Worksheet, commandService: ICommandService) => {
if (!commandService.disposed) {
const command = followSelectionOperation(range, workbook, worksheet);
commandService.executeCommand(command.id, command.params);
};
}, 300);
export const RemoveRowCommandId = 'sheet.command.remove-row';
export const RemoveRowByRangeCommand: ICommand<IRemoveRowByRangeCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-row-by-range',
// eslint-disable-next-line max-lines-per-function
handler: (accessor, params) => {
if (!params) {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { workbook, worksheet } = target;
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const { range, unitId, subUnitId } = params;
const visibleRanges = getVisibleRanges([range], accessor, unitId, subUnitId).reverse();
const undoMutations: IMutationInfo[] = [];
const redoMutations: IMutationInfo[] = [];
visibleRanges.forEach((visibleRange) => {
const undos: IMutationInfo[] = [];
const redos: IMutationInfo[] = [];
const removeRowsParams: IRemoveRowsMutationParams = {
unitId,
subUnitId,
range: visibleRange,
};
const undoRemoveRowsParams: IInsertRowMutationParams = RemoveRowsUndoMutationFactory(
removeRowsParams,
worksheet
);
const removedRows = worksheet.getCellMatrix().getSlice(visibleRange.startRow, visibleRange.endRow, 0, worksheet.getColumnCount() - 1);
const undoSetRangeValuesParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: removedRows.getMatrix(),
};
redos.push({ id: RemoveRowMutation.id, params: removeRowsParams });
undos.push({ id: InsertRowMutation.id, params: undoRemoveRowsParams });
undos.push({ id: SetRangeValuesMutation.id, params: undoSetRangeValuesParams });
redoMutations.push(...redos);
undoMutations.unshift(...undos);
});
const interceptorParams: IRemoveRowColCommandInterceptParams = params;
const intercepted = sheetInterceptorService.onCommandExecute({
id: RemoveRowCommandId,
params: interceptorParams,
});
const commandService = accessor.get(ICommandService);
const result = sequenceExecute(
[
...(intercepted.preRedos ?? []),
...redoMutations,
...intercepted.redos,
],
commandService
);
if (result.result) {
setSelection(range, workbook, worksheet, commandService);
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: RemoveRowCommandId,
params: interceptorParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [
...(intercepted.preUndos ?? []),
...undoMutations,
...intercepted.undos,
...afterInterceptors.undos,
],
redoMutations: [
...(intercepted.preRedos ?? []),
...redoMutations,
...intercepted.redos,
...afterInterceptors.redos,
],
});
return true;
}
return false;
},
};
/**
* This command would remove the selected rows. These selected rows can be non-continuous.
*/
export const RemoveRowCommand: ICommand<IRemoveRowColCommandParams> = {
type: CommandType.COMMAND,
id: RemoveRowCommandId,
handler: async (accessor: IAccessor, params?: IRemoveRowColCommandParams) => {
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const selectionManagerService = accessor.get(SheetsSelectionsService);
let range = params?.range || selectionManagerService.getCurrentLastSelection()?.range;
if (!range) return false;
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const commandService = accessor.get(ICommandService);
const { worksheet, unitId, subUnitId } = target;
range = {
...range,
startColumn: 0,
endColumn: Math.max(worksheet.getMaxColumns() - 1, 0),
};
const canPerform = await sheetInterceptorService.beforeCommandExecute({
id: RemoveRowCommand.id,
params: { range, unitId, subUnitId },
});
if (!canPerform) {
return false;
}
return commandService.syncExecuteCommand(RemoveRowByRangeCommand.id, {
range,
unitId,
subUnitId,
});
},
};
export const RemoveColCommandId = 'sheet.command.remove-col';
export const RemoveColByRangeCommand: ICommand<IRemoveColByRangeCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-col-by-range',
handler: (accessor, params) => {
if (!params) {
return false;
}
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { workbook, worksheet } = target;
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const { range, unitId, subUnitId } = params;
// col count
const removeColParams: IRemoveColMutationParams = {
unitId,
subUnitId,
range,
};
const undoRemoveColParams: IInsertColMutationParams = RemoveColMutationFactory(accessor, removeColParams);
const removedCols = worksheet.getCellMatrix().getSlice(0, worksheet.getRowCount() - 1, range.startColumn, range.endColumn);
const undoSetRangeValuesParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: removedCols.getMatrix(),
};
const interceptorParams: IRemoveRowColCommandInterceptParams = params;
const intercepted = sheetInterceptorService.onCommandExecute({
id: RemoveColCommandId,
params: interceptorParams,
});
const commandService = accessor.get(ICommandService);
const result = sequenceExecute(
[
...(intercepted.preRedos ?? []),
{ id: RemoveColMutation.id, params: removeColParams },
...intercepted.redos,
],
commandService
);
if (result.result) {
setSelection(range, workbook, worksheet, commandService);
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: RemoveColCommandId,
params: interceptorParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [
...(intercepted.preUndos ?? []),
{ id: InsertColMutation.id, params: undoRemoveColParams },
{ id: SetRangeValuesMutation.id, params: undoSetRangeValuesParams },
...intercepted.undos,
...afterInterceptors.undos,
],
redoMutations: [
...(intercepted.preRedos ?? []),
{ id: RemoveColMutation.id, params: removeColParams },
...intercepted.redos,
...afterInterceptors.redos,
],
});
return true;
}
return false;
},
};
/**
* This command would remove the selected columns. These selected rows can be non-continuous.
*/
export const RemoveColCommand: ICommand = {
type: CommandType.COMMAND,
id: RemoveColCommandId,
handler: async (accessor: IAccessor, params?: IRemoveRowColCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const commandService = accessor.get(ICommandService);
let range = params?.range;
if (!range) range = selectionManagerService.getCurrentLastSelection()?.range;
if (!range) return false;
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { worksheet, subUnitId, unitId } = target;
range = {
...range,
startRow: 0,
endRow: Math.max(worksheet.getMaxRows() - 1, 0),
};
const canPerform = await sheetInterceptorService.beforeCommandExecute({
id: RemoveColCommand.id,
params: { range, unitId, subUnitId },
});
if (!canPerform) {
return false;
}
return commandService.syncExecuteCommand(RemoveColByRangeCommand.id, {
range,
unitId,
subUnitId,
});
},
};

View File

@@ -0,0 +1,109 @@
/**
* 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, ICommand } from '@univerjs/core';
import type {
IInsertSheetMutationParams,
IRemoveSheetMutationParams,
} from '../../basics/interfaces/mutation-interface';
import type { IUniverSheetsConfig } from '../../config/config';
import {
CommandType,
ICommandService,
IConfigService,
IUndoRedoService,
IUniverInstanceService,
sequenceExecute,
} from '@univerjs/core';
import { defaultLargeSheetOperationConfig, SHEETS_PLUGIN_CONFIG_KEY } from '../../config/config';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { InsertSheetMutation } from '../mutations/insert-sheet.mutation';
import { RemoveSheetMutation, RemoveSheetUndoMutationFactory } from '../mutations/remove-sheet.mutation';
import { countCells } from './util';
import { getSheetCommandTarget } from './utils/target-util';
export interface IRemoveSheetCommandParams {
unitId?: string;
subUnitId: string;
}
/**
* The command to insert new worksheet
*/
export const RemoveSheetCommand: ICommand = {
id: 'sheet.command.remove-sheet',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IRemoveSheetCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const configService = accessor.get(IConfigService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { unitId, subUnitId, workbook, worksheet } = target;
if (workbook.getSheets().length <= 1) return false;
// Check if this is a large sheet that shouldn't support undo/redo
const pluginConfig = configService.getConfig<IUniverSheetsConfig>(SHEETS_PLUGIN_CONFIG_KEY);
const largeSheetConfig = {
...defaultLargeSheetOperationConfig,
...pluginConfig?.largeSheetOperation,
};
const cellCount = countCells(worksheet.getCellMatrix());
const isLargeSheet = cellCount >= largeSheetConfig.largeSheetCellCountThreshold;
// prepare do mutations
const RemoveSheetMutationParams: IRemoveSheetMutationParams = {
subUnitId,
unitId,
subUnitName: worksheet.getName(),
};
// For large sheets, we don't need to prepare undo mutation since undo/redo won't be supported
const InsertSheetMutationParams: IInsertSheetMutationParams | null = isLargeSheet
? null
: RemoveSheetUndoMutationFactory(accessor, RemoveSheetMutationParams);
const intercepted = sheetInterceptorService.onCommandExecute({
id: RemoveSheetCommand.id,
params: { unitId, subUnitId },
});
const redos = [...(intercepted.preRedos ?? []), { id: RemoveSheetMutation.id, params: RemoveSheetMutationParams }, ...intercepted.redos];
const undos = isLargeSheet
? []
: [...(intercepted.preUndos ?? []), { id: InsertSheetMutation.id, params: InsertSheetMutationParams! }, ...intercepted.undos];
const result = sequenceExecute(redos, commandService);
if (result.result) {
if (isLargeSheet) {
// For large sheets, clear undo/redo to disable undo/redo functionality
undoRedoService.clearUndoRedo(unitId);
} else {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: undos,
redoMutations: redos,
});
}
return true;
}
return false;
},
};

View File

@@ -0,0 +1,157 @@
/**
* 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, ICommand, IMutationInfo, IRange, Nullable, Worksheet } from '@univerjs/core';
import type { IAddWorksheetMergeMutationParams, IRemoveWorksheetMergeMutationParams } from '../../basics/interfaces/mutation-interface';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, ObjectMatrix, Rectangle, sequenceExecute, Tools } from '@univerjs/core';
import { SelectionMoveType } from '../../services/selections';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { AddWorksheetMergeMutation } from '../mutations/add-worksheet-merge.mutation';
import { RemoveMergeUndoMutationFactory, RemoveWorksheetMergeMutation } from '../mutations/remove-worksheet-merge.mutation';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { SetSelectionsOperation } from '../operations/selection.operation';
import { getSheetCommandTarget } from './utils/target-util';
export interface IRemoveWorksheetMergeCommandParams extends ISheetCommandSharedParams {
ranges?: IRange[];
}
export const RemoveWorksheetMergeCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.remove-worksheet-merge',
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params: IRemoveWorksheetMergeCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const selections = params?.ranges || selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!selections?.length) return false;
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const { subUnitId, unitId, worksheet } = target;
const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = {
unitId,
subUnitId,
ranges: selections,
};
const mergeData = worksheet.getConfig().mergeData;
const intersectsMerges = mergeData.filter((merge) => {
return selections.some((selection) => Rectangle.intersects(selection, merge));
});
if (!intersectsMerges.length) return false;
const undoRedoMutationParams: IAddWorksheetMergeMutationParams = RemoveMergeUndoMutationFactory(
accessor,
removeMergeMutationParams
);
const nowSelections = selectionManagerService.getCurrentSelections();
const undoSelections = Tools.deepClone(nowSelections);
const redoSelections = Tools.deepClone(nowSelections);
if (nowSelections.length) {
const redoLastSelection = redoSelections[redoSelections.length - 1];
const { startRow, startColumn } = redoLastSelection.range;
redoLastSelection.primary = {
startRow,
startColumn,
endRow: startRow,
endColumn: startColumn,
actualRow: startRow,
actualColumn: startColumn,
isMerged: false,
isMergedMainCell: false,
};
}
const getSetRangeValuesParams = getSetRangeStyleParamsForRemoveMerge(worksheet, intersectsMerges);
const redoSetRangeValueParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: getSetRangeValuesParams.getMatrix(),
};
const undoSetRangeValueParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
redoSetRangeValueParams
);
const redoMutations: IMutationInfo[] = [
{ id: RemoveWorksheetMergeMutation.id, params: undoRedoMutationParams },
{ id: SetRangeValuesMutation.id, params: redoSetRangeValueParams },
{
id: SetSelectionsOperation.id,
params: {
unitId,
subUnitId,
selections: redoSelections,
type: SelectionMoveType.ONLY_SET,
},
},
];
const undoMutations: IMutationInfo[] = [
{ id: AddWorksheetMergeMutation.id, params: undoRedoMutationParams },
{ id: SetRangeValuesMutation.id, params: undoSetRangeValueParams },
{
id: SetSelectionsOperation.id,
params: {
unitId,
subUnitId,
selections: undoSelections,
type: SelectionMoveType.ONLY_SET,
},
},
];
const result = sequenceExecute(redoMutations, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations,
redoMutations,
});
return true;
}
return false;
},
};
function getSetRangeStyleParamsForRemoveMerge(worksheet: Worksheet, ranges: IRange[]) {
const styleRedoMatrix = new ObjectMatrix<Nullable<ICellData>>();
ranges.forEach((range) => {
const { startRow, startColumn, endColumn, endRow } = range;
const cellValue = worksheet.getCellMatrix().getValue(startRow, startColumn);
if (cellValue?.s) {
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
if (i !== startRow || j !== startColumn) {
styleRedoMatrix.setValue(i, j, { s: cellValue.s });
}
}
}
}
});
return styleRedoMatrix;
}

View File

@@ -0,0 +1,94 @@
/**
* 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, ICommand, IRange } from '@univerjs/core';
import type { IReorderRangeMutationParams } from '../mutations/reorder-range.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import { SheetSkeletonService } from '../../skeleton/skeleton.service';
import { ReorderRangeMutation, ReorderRangeUndoMutationFactory } from '../mutations/reorder-range.mutation';
import { getSuitableRangesInView } from './util';
export interface IReorderRangeCommandParams extends ISheetCommandSharedParams {
range: IRange;
order: { [key: number]: number };
}
export const ReorderRangeCommandId = 'sheet.command.reorder-range' as const;
export const ReorderRangeCommand: ICommand<IReorderRangeCommandParams> = {
id: ReorderRangeCommandId,
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: IReorderRangeCommandParams) => {
const { subUnitId, unitId, range, order } = params;
const commandService = accessor.get(ICommandService);
const reorderMutation = {
id: ReorderRangeMutation.id,
params: {
unitId,
subUnitId,
order,
range,
} as IReorderRangeMutationParams,
};
const undoReorderMutation = {
id: ReorderRangeMutation.id,
params: ReorderRangeUndoMutationFactory(reorderMutation.params),
};
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: ReorderRangeCommand.id, params });
const redos = [
...(interceptorCommands.preRedos ?? []),
reorderMutation,
...interceptorCommands.redos,
];
const undos = [
...(interceptorCommands.preUndos ?? []),
undoReorderMutation,
...interceptorCommands.undos,
];
const result = sequenceExecute(redos, commandService);
const { suitableRanges, remainingRanges } = getSuitableRangesInView([range], accessor.get(SheetSkeletonService).getSkeleton(unitId, subUnitId));
const { undos: autoHeightUndos, redos: autoHeightRedos } = sheetInterceptorService.generateMutationsOfAutoHeight({
unitId,
subUnitId,
ranges: [range],
autoHeightRanges: suitableRanges,
lazyAutoHeightRanges: remainingRanges,
});
const reorderAfterIntercepted = sheetInterceptorService.afterCommandExecute({ id: ReorderRangeCommand.id, params });
if (result.result) {
sequenceExecute([...reorderAfterIntercepted.redos, ...autoHeightRedos], commandService);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [...undos, ...reorderAfterIntercepted.undos, ...autoHeightUndos],
redoMutations: [...redos, ...reorderAfterIntercepted.redos, ...autoHeightRedos],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,651 @@
/**
* 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 {
BorderStyleTypes,
IAccessor,
IBorderData,
IBorderStyleData,
ICellData,
ICommand,
IRange,
IStyleData,
} from '@univerjs/core';
import type { IBorderInfo } from '../../services/border-style-manager.service';
import type { ISetRangeValuesMutationParams } from '../mutations/set-range-values.mutation';
import type { IResult } from './utils/target-util';
import {
BorderType,
CommandType,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
ObjectMatrix,
Tools,
} from '@univerjs/core';
import { BorderStyleManagerService } from '../../services/border-style-manager.service';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '../mutations/set-range-values.mutation';
import { getSheetCommandTarget } from './utils/target-util';
function forEach(range: IRange, action: (row: number, column: number) => void): void {
const { startRow, startColumn, endRow, endColumn } = range;
for (let i = startRow; i <= endRow; i++) {
for (let j = startColumn; j <= endColumn; j++) {
action(i, j);
}
}
}
export interface ISetBorderBasicCommandParams {
unitId?: string;
subUnitId?: string;
ranges: IRange[];
value: IBorderInfo;
}
export interface ISetBorderPositionCommandParams {
value: BorderType;
}
export interface ISetBorderStyleCommandParams {
value: BorderStyleTypes;
}
export interface ISetBorderCommandParams {
unitId?: string;
subUnitId?: string;
ranges?: IRange[];
}
export interface ISetBorderColorCommandParams {
value: string;
}
const setBorderStyleForRange = (borderContext: ReturnType<typeof getBorderContext>, range: IRange, defaultStyle: IBorderData, reserve?: boolean) => {
const { mr, worksheet } = borderContext;
if (range.startRow < 0 || range.startColumn < 0) return;
forEach(range, (row, column) => {
const rectangle = worksheet.getMergedCell(row, column);
let bdStyle = defaultStyle;
if (rectangle && (defaultStyle.bc_tr || defaultStyle.ml_tr || defaultStyle.bl_tr || defaultStyle.tl_mr || defaultStyle.tl_bc || defaultStyle.tl_br)) {
if (reserve) {
const style = Tools.deepClone(
mr.getValue(rectangle.startRow, rectangle.startColumn)?.s
) as IStyleData;
bdStyle = style?.bd ? Object.assign(style.bd, defaultStyle) : defaultStyle;
}
mr.setValue(rectangle.startRow, rectangle.startColumn, {
s: {
bd: bdStyle,
},
});
} else {
if (reserve) {
const style = Tools.deepClone(mr.getValue(row, column)?.s) as IStyleData;
bdStyle = style?.bd ? Object.assign(style.bd, defaultStyle) : defaultStyle;
}
mr.setValue(row, column, { s: { bd: bdStyle } });
}
});
};
const prepareEdgeRange = (range: IRange) => {
const topRangeOut = {
startRow: range.startRow - 1,
startColumn: range.startColumn,
endRow: range.startRow - 1,
endColumn: range.endColumn,
};
const leftRangeOut = {
startRow: range.startRow,
startColumn: range.startColumn - 1,
endRow: range.endRow,
endColumn: range.startColumn - 1,
};
const bottomRangeOut = {
startRow: range.endRow + 1,
startColumn: range.startColumn,
endRow: range.endRow + 1,
endColumn: range.endColumn,
};
const rightRangeOut = {
startRow: range.startRow,
startColumn: range.endColumn + 1,
endRow: range.endRow,
endColumn: range.endColumn + 1,
};
// Cells in the upper, lower, left and right ranges
const topRange = {
startRow: range.startRow,
startColumn: range.startColumn,
endRow: range.startRow,
endColumn: range.endColumn,
};
const leftRange = {
startRow: range.startRow,
startColumn: range.startColumn,
endRow: range.endRow,
endColumn: range.startColumn,
};
const bottomRange = {
startRow: range.endRow,
startColumn: range.startColumn,
endRow: range.endRow,
endColumn: range.endColumn,
};
const rightRange = {
startRow: range.startRow,
startColumn: range.endColumn,
endRow: range.endRow,
endColumn: range.endColumn,
};
return {
topRangeOut,
leftRangeOut,
bottomRangeOut,
rightRangeOut,
topRange,
leftRange,
bottomRange,
rightRange,
};
};
function getBorderContext(borderStyleManagerService: BorderStyleManagerService, target: IResult, selections: IRange[]) {
const { style, color, type } = borderStyleManagerService.getBorderInfo();
const top = type === BorderType.TOP || type === BorderType.ALL || type === BorderType.OUTSIDE;
const left = type === BorderType.LEFT || type === BorderType.ALL || type === BorderType.OUTSIDE;
const bottom = type === BorderType.BOTTOM || type === BorderType.ALL || type === BorderType.OUTSIDE;
const right = type === BorderType.RIGHT || type === BorderType.ALL || type === BorderType.OUTSIDE;
const vertical = type === BorderType.VERTICAL || type === BorderType.ALL || type === BorderType.INSIDE;
const horizontal = type === BorderType.HORIZONTAL || type === BorderType.ALL || type === BorderType.INSIDE;
const tl_br = type.indexOf('tlbr') > -1;
const tl_bc = type.indexOf('tlbc') > -1;
const tl_mr = type.indexOf('tlmr') > -1;
const bl_tr = type.indexOf('bltr') > -1;
const ml_tr = type.indexOf('mltr') > -1;
const bc_tr = type.indexOf('bctr') > -1;
const range = selections[0]; // TODO @Dushusir support multiple ranges
// Cells in the surrounding range may need to clear the border
const {
topRangeOut,
leftRangeOut,
bottomRangeOut,
rightRangeOut,
topRange,
leftRange,
bottomRange,
rightRange,
} = prepareEdgeRange(range);
const mr = new ObjectMatrix<ICellData>();
const { worksheet, unitId, subUnitId } = target;
const borderStyle: IBorderStyleData = {
s: style,
cl: {
rgb: color,
},
};
return {
worksheet,
unitId,
subUnitId,
style,
color,
type,
top,
left,
right,
bottom,
vertical,
horizontal,
tl_br,
tl_bc,
tl_mr,
bl_tr,
ml_tr,
bc_tr,
topRangeOut,
leftRangeOut,
bottomRangeOut,
rightRangeOut,
topRange,
leftRange,
bottomRange,
rightRange,
range,
mr,
borderStyle,
};
}
// const setBorderStyle = (borderContext: ReturnType<typeof getBorderContext>, defaultStyle: IBorderData, reserve?: boolean) => {
// setBorderStyleForRange(borderContext.mr, borderContext.worksheet, borderContext.range, defaultStyle, reserve);
// };
// eslint-disable-next-line max-lines-per-function
const innerBorder = (borderContext: ReturnType<typeof getBorderContext>) => {
const { range, mr, borderStyle, vertical, horizontal, worksheet } = borderContext;
//#region inner vertical border
if (vertical) {
forEach(range, (row, column) => {
const mergedRange = worksheet.getMergedCell(row, column);
if (mergedRange) {
const topLeftStyle = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
if (mergedRange.startColumn !== range.startColumn) {
mr.setValue(row, column, {
s: {
bd: topLeftStyle?.bd
? Object.assign(topLeftStyle.bd, { l: Tools.deepClone(borderStyle) })
: { l: Tools.deepClone(borderStyle) },
},
});
}
} else {
if (column !== range.endColumn) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd
? Object.assign(style.bd, { r: Tools.deepClone(borderStyle) })
: { r: Tools.deepClone(borderStyle) },
},
});
}
if (column !== range.startColumn) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd
? Object.assign(style.bd, { l: Tools.deepClone(borderStyle) })
: { l: Tools.deepClone(borderStyle) },
},
});
}
}
});
}
//#endregion
//#region inner horizontal border
if (horizontal) {
forEach(range, (row, column) => {
const mergedRange = worksheet.getMergedCell(row, column);
if (mergedRange) {
const topLeftStyle = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
if (mergedRange.startRow !== range.startRow) {
mr.setValue(row, column, {
s: {
bd: topLeftStyle?.bd
? Object.assign(topLeftStyle.bd, { t: Tools.deepClone(borderStyle) })
: { t: Tools.deepClone(borderStyle) },
},
});
}
} else {
if (row !== range.endRow) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd
? Object.assign(style.bd, { b: Tools.deepClone(borderStyle) })
: { b: Tools.deepClone(borderStyle) },
},
});
}
if (row !== range.startRow) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd
? Object.assign(style.bd, { t: Tools.deepClone(borderStyle) })
: { t: Tools.deepClone(borderStyle) },
},
});
}
}
});
}
//#endregion
};
function otherBorders(borderContext: ReturnType<typeof getBorderContext>) {
const { borderStyle, tl_br, tl_bc, tl_mr, bl_tr, ml_tr, bc_tr } = borderContext;
const setBorderStyle = (range: IRange, defaultStyle: IBorderData, reserve?: boolean) => {
setBorderStyleForRange(borderContext, range, defaultStyle, reserve);
};
if (tl_br) {
setBorderStyle(borderContext.range, { tl_br: Tools.deepClone(borderStyle) }, true);
}
if (tl_bc) {
setBorderStyle(borderContext.range, { tl_bc: Tools.deepClone(borderStyle) }, true);
}
if (tl_mr) {
setBorderStyle(borderContext.range, { tl_mr: Tools.deepClone(borderStyle) }, true);
}
if (bl_tr) {
setBorderStyle(borderContext.range, { bl_tr: Tools.deepClone(borderStyle) }, true);
}
if (ml_tr) {
setBorderStyle(borderContext.range, { ml_tr: Tools.deepClone(borderStyle) }, true);
}
if (bc_tr) {
setBorderStyle(borderContext.range, { bc_tr: Tools.deepClone(borderStyle) }, true);
}
}
const outlineBorder = (borderContext: ReturnType<typeof getBorderContext>) => {
const { top, left, right, bottom, borderStyle, bottomRange, topRange, leftRange, rightRange, bottomRangeOut, topRangeOut, leftRangeOut, rightRangeOut } = borderContext;
const setBorderStyle = (range: IRange, defaultStyle: IBorderData, reserve?: boolean) => {
setBorderStyleForRange(borderContext, range, defaultStyle, reserve);
};
if (top) {
/**
* pro/issues/344
* Compatible with Excel's border rendering.
* When the top border of a cell and the bottom border of the cell above it (r-1) overlap,
* if the top border of cell r is white, then the rendering is ignored.
*/
setBorderStyle(topRangeOut, { b: null });
setBorderStyle(topRange, { t: Tools.deepClone(borderStyle) }, true);
}
if (bottom) {
//see univer-pro/issues/2561
// add bottom line for merged cell
// must put below inner horizontal border, horizontal border would clear right border when using topleftBorderStyle
setBorderStyle(bottomRangeOut, { t: null });
setBorderStyle(bottomRange, { b: Tools.deepClone(borderStyle) }, true);
}
if (left) {
setBorderStyle(leftRangeOut, { r: null });
setBorderStyle(leftRange, { l: Tools.deepClone(borderStyle) }, true);
}
if (right) {
//see univer-pro/issues/2561
// add bottom line for merged cell
// must put below inner vertical border, vertical border would clear right border when using topleftBorderStyle
setBorderStyle(rightRangeOut, { l: null });
setBorderStyle(rightRange, { r: Tools.deepClone(borderStyle) }, true);
}
};
// eslint-disable-next-line max-lines-per-function
const clearBorder = (borderContext: ReturnType<typeof getBorderContext>) => {
const { range, worksheet, mr, top, bottom, left, right, vertical, horizontal, tl_br, tl_bc, tl_mr, bl_tr, ml_tr, bc_tr, topRange, bottomRange, leftRange, rightRange, topRangeOut, bottomRangeOut, leftRangeOut, rightRangeOut } = borderContext;
const setBorderStyle = (range: IRange, defaultStyle: IBorderData, reserve?: boolean) => {
setBorderStyleForRange(borderContext, range, defaultStyle, reserve);
};
if (
!top &&
!bottom &&
!left &&
!right &&
!vertical &&
!horizontal &&
!tl_br &&
!tl_bc &&
!tl_mr &&
!bl_tr &&
!ml_tr &&
!bc_tr
) {
// eslint-disable-next-line complexity
forEach(range, (row, column) => {
const mergedRange = worksheet.getMergedCell(row, column);
// If this cell has border setting before merge, then we need clear settings of edge border.
if (mergedRange) {
// Remove the right border from all cells except for the right border of the rightmost cell.
if (mergedRange.endColumn !== range.endColumn) {
const style = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { r: null }) : { r: null },
},
});
}
// Remove the left border from all cells except for the left border of the leftmost cell.
if (mergedRange.startColumn !== range.startColumn) {
const style = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { l: null }) : { l: null },
},
});
}
// see https://github.com/dream-num/univer/pull/3506
// Remove the top border from all cells except for the top border of the topmost cell.
if (mergedRange.endRow !== range.endRow) {
const style = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { b: null }) : { b: null },
},
});
}
// Remove the bottom border from all cells except for the bottom border of the bottommost cell.
if (mergedRange.startRow !== range.startRow) {
const style = mr.getValue(mergedRange.startRow, mergedRange.startColumn)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { t: null }) : { t: null },
},
});
}
} else {
// Clear the right border of all columns except the last column
if (column !== range.endColumn) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { r: null }) : { r: null },
},
});
}
// Clear the left border of all columns except the first column
if (column !== range.startColumn) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { l: null }) : { l: null },
},
});
}
// Clear all the bottom border except the last line
if (row !== range.endRow) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { b: null }) : { b: null },
},
});
}
// Clear the top border of all lines except the first line
if (row !== range.startRow) {
const style = mr.getValue(row, column)?.s as IStyleData;
mr.setValue(row, column, {
s: {
bd: style?.bd ? Object.assign(style.bd, { t: null }) : { t: null },
},
});
}
}
});
setBorderStyle(topRangeOut, { b: null });
setBorderStyle(topRange, { t: null }, true);
setBorderStyle(bottomRangeOut, { t: null });
setBorderStyle(bottomRange, { b: null }, true);
setBorderStyle(leftRangeOut, { r: null });
setBorderStyle(leftRange, { l: null }, true);
setBorderStyle(rightRangeOut, { l: null });
setBorderStyle(rightRange, { r: null }, true);
setBorderStyle(range, { tl_br: null }, true);
setBorderStyle(range, { tl_bc: null }, true);
setBorderStyle(range, { tl_mr: null }, true);
setBorderStyle(range, { bl_tr: null }, true);
setBorderStyle(range, { ml_tr: null }, true);
setBorderStyle(range, { bc_tr: null }, true);
}
};
/**
* Set border info for range, including clear border (type = NONE)
*/
export const SetBorderCommand: ICommand = {
id: 'sheet.command.set-border',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: ISetBorderCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const selectionManagerService = accessor.get(SheetsSelectionsService);
const borderStyleManagerService = accessor.get(BorderStyleManagerService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const ranges = params?.ranges || selectionManagerService.getCurrentSelections()?.map((s) => s.range);
if (!ranges?.length) {
return false;
}
const { activeBorderType } = borderStyleManagerService.getBorderInfo();
if (!activeBorderType) return false;
const borderContext = getBorderContext(borderStyleManagerService, target, ranges);
innerBorder(borderContext);
outlineBorder(borderContext);
otherBorders(borderContext);
clearBorder(borderContext);
const { unitId, subUnitId, mr } = borderContext;
const setRangeValuesMutationParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: mr.clone(),
};
const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
accessor,
setRangeValuesMutationParams
);
// execute do mutations and add undo mutations to undo stack if completed
const result = commandService.syncExecuteCommand(SetRangeValuesMutation.id, setRangeValuesMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: SetRangeValuesMutation.id, params: undoSetRangeValuesMutationParams }],
redoMutations: [{ id: SetRangeValuesMutation.id, params: setRangeValuesMutationParams }],
});
return true;
}
return false;
},
};
export const SetBorderPositionCommand: ICommand<ISetBorderPositionCommandParams> = {
id: 'sheet.command.set-border-position',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISetBorderPositionCommandParams) => {
if (!params.value) return false;
const commandService = accessor.get(ICommandService);
const borderManager = accessor.get(BorderStyleManagerService);
borderManager.setType(params.value);
return commandService.syncExecuteCommand(SetBorderCommand.id);
},
};
export const SetBorderStyleCommand: ICommand = {
id: 'sheet.command.set-border-style',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISetBorderStyleCommandParams) => {
const commandService = accessor.get(ICommandService);
const borderManager = accessor.get(BorderStyleManagerService);
borderManager.setStyle(params.value);
return commandService.syncExecuteCommand(SetBorderCommand.id);
},
};
export const SetBorderColorCommand: ICommand<ISetBorderColorCommandParams> = {
id: 'sheet.command.set-border-color',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISetBorderColorCommandParams) => {
const commandService = accessor.get(ICommandService);
const borderManager = accessor.get(BorderStyleManagerService);
borderManager.setColor(params.value);
return commandService.syncExecuteCommand(SetBorderCommand.id);
},
};
export const SetBorderBasicCommand: ICommand<ISetBorderBasicCommandParams> = {
id: 'sheet.command.set-border-basic',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISetBorderBasicCommandParams) => {
const { unitId, subUnitId, value, ranges } = params;
const { type, color, style } = value;
const commandService = accessor.get(ICommandService);
const borderManager = accessor.get(BorderStyleManagerService);
borderManager.setType(type);
if (color) borderManager.setColor(color);
borderManager.setStyle(style);
return commandService.syncExecuteCommand(SetBorderCommand.id, {
unitId,
subUnitId,
ranges,
});
},
};

View File

@@ -0,0 +1,71 @@
/**
* 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, IColumnData, ICommand, IObjectArrayPrimitiveType, Nullable } from '@univerjs/core';
import type { ISetColDataMutationParams } from '../mutations/set-col-data.mutation';
import type { ISheetCommandSharedParams } from '../utils/interface';
import {
CommandType,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
} from '@univerjs/core';
import { SetColDataMutation, SetColDataMutationFactory } from '../mutations/set-col-data.mutation';
import { getSheetCommandTarget } from './utils/target-util';
export type IColumnProperties = Omit<IColumnData, 'w' | 'hd'>;
export interface ISetColDataCommandParams extends Partial<ISheetCommandSharedParams> {
columnData: IObjectArrayPrimitiveType<Nullable<IColumnProperties>>;
}
export const SetColDataCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.set-col-data',
handler: (accessor: IAccessor, params: ISetColDataCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { columnData } = params;
const { unitId, subUnitId, worksheet } = target;
const redoMutationParams: ISetColDataMutationParams = {
subUnitId,
unitId,
columnData,
};
const undoMutationParams = SetColDataMutationFactory(redoMutationParams, worksheet);
const result = commandService.syncExecuteCommand(SetColDataMutation.id, redoMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: SetColDataMutation.id, params: undoMutationParams }],
redoMutations: [{ id: SetColDataMutation.id, params: redoMutationParams }],
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,337 @@
/**
* 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, ICommand, IRange, Nullable, Worksheet } from '@univerjs/core';
import type { ISetColHiddenMutationParams, ISetColVisibleMutationParams } from '../mutations/set-col-visible.mutation';
import type { ISetSelectionsOperationParams } from '../operations/selection.operation';
import {
CommandType,
ICommandService,
IUndoRedoService,
IUniverInstanceService,
RANGE_TYPE,
sequenceExecute,
} from '@univerjs/core';
import { SheetsSelectionsService } from '../../services/selections/selection.service';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
import {
SetColHiddenMutation,
SetColHiddenUndoMutationFactory,
SetColVisibleMutation,
SetColVisibleUndoMutationFactory,
} from '../mutations/set-col-visible.mutation';
import { SetSelectionsOperation } from '../operations/selection.operation';
import { getPrimaryForRange } from './utils/selection-utils';
import { getSheetCommandTarget } from './utils/target-util';
export interface ISetSpecificColsVisibleCommandParams {
unitId: string;
subUnitId: string;
ranges: IRange[];
}
export const SetSpecificColsVisibleCommand: ICommand<ISetSpecificColsVisibleCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.set-col-visible-on-cols',
handler: (accessor, params: ISetSpecificColsVisibleCommandParams) => {
const { unitId, subUnitId, ranges } = params;
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const commandService = accessor.get(ICommandService);
const instanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(instanceService, { unitId, subUnitId });
if (!target) return false;
const { worksheet } = target;
const redoMutationParams: ISetColVisibleMutationParams = {
unitId,
subUnitId,
ranges,
};
const setSelectionOperationParams: ISetSelectionsOperationParams = {
unitId,
subUnitId,
reveal: true,
selections: ranges.map((r) => ({ range: r, primary: getPrimaryForRange(r, worksheet), style: null })),
};
const undoMutationParams = SetColVisibleUndoMutationFactory(accessor, redoMutationParams);
const undoSetSelectionsOperationParams: ISetSelectionsOperationParams = {
unitId,
subUnitId,
selections: getSelectionsAfterHiding(ranges).map((range) => ({
range,
primary: getPrimaryForRange(range, worksheet),
style: null,
})),
};
const result = sequenceExecute([
{ id: SetColVisibleMutation.id, params: redoMutationParams },
{ id: SetSelectionsOperation.id, params: setSelectionOperationParams },
], commandService);
const intercepted = sheetInterceptorService.onCommandExecute({
id: SetSpecificColsVisibleCommand.id,
params,
});
const interceptedResult = sequenceExecute([...intercepted.redos], commandService);
if (result.result && interceptedResult.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: SetSpecificColsVisibleCommand.id,
params,
});
sequenceExecute(afterInterceptors.redos, commandService);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [
{ id: SetColHiddenMutation.id, params: undoMutationParams },
{ id: SetSelectionsOperation.id, params: undoSetSelectionsOperationParams },
...(intercepted.undos ?? []),
...afterInterceptors.undos,
],
redoMutations: [
...(intercepted.preRedos ?? []),
{ id: SetColVisibleMutation.id, params: redoMutationParams },
{ id: SetSelectionsOperation.id, params: setSelectionOperationParams },
...intercepted.redos,
...afterInterceptors.redos,
],
});
return true;
}
return true;
},
};
export const SetSelectedColsVisibleCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.set-selected-cols-visible',
handler: (accessor: IAccessor) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const commandService = accessor.get(ICommandService);
const ranges = getSelectedColRanges(selectionManagerService);
if (!ranges.length) return false;
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
// `ranges` would not overlap each other, so `hiddenRanges` would not overlap each other either
const hiddenRanges = getHiddenColRanges(worksheet, ranges);
return commandService.executeCommand<ISetSpecificColsVisibleCommandParams>(SetSpecificColsVisibleCommand.id, {
unitId,
subUnitId,
ranges: hiddenRanges,
});
},
};
export interface ISetColHiddenCommandParams {
unitId?: string;
subUnitId?: string;
ranges?: IRange[];
}
export const SetColHiddenCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.set-col-hidden',
handler: (accessor: IAccessor, params?: ISetColHiddenCommandParams) => {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
let ranges = params?.ranges?.length ? params.ranges : getSelectedColRanges(selectionManagerService);
if (!ranges.length) return false;
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { worksheet, unitId, subUnitId } = target;
ranges = divideRangesByHiddenCols(target.worksheet, ranges);
const redoMutationParams: ISetColHiddenMutationParams = { unitId, subUnitId, ranges };
const setSelectionOperationParams: ISetSelectionsOperationParams = {
unitId,
subUnitId,
selections: getSelectionsAfterHiding(ranges).map((range) => ({
range,
primary: getPrimaryForRange(range, worksheet),
style: null,
})),
};
const undoMutationParams = SetColHiddenUndoMutationFactory(accessor, redoMutationParams);
const undoSetSelectionsOperationParams: ISetSelectionsOperationParams = {
unitId,
subUnitId,
reveal: true,
selections: ranges.map((range) => ({
range,
primary: getPrimaryForRange(range, worksheet),
style: null,
})),
};
const result = sequenceExecute([
{ id: SetColHiddenMutation.id, params: redoMutationParams },
{ id: SetSelectionsOperation.id, params: setSelectionOperationParams },
], commandService);
const intercepted = sheetInterceptorService.onCommandExecute({
id: SetColHiddenCommand.id,
params: redoMutationParams,
});
const interceptedResult = sequenceExecute([...intercepted.redos], commandService);
if (result.result && interceptedResult.result) {
const afterInterceptors = sheetInterceptorService.afterCommandExecute({
id: SetColHiddenCommand.id,
params: redoMutationParams,
});
sequenceExecute(afterInterceptors.redos, commandService);
const undoRedoService = accessor.get(IUndoRedoService);
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [
{ id: SetColVisibleMutation.id, params: undoMutationParams },
{ id: SetSelectionsOperation.id, params: undoSetSelectionsOperationParams },
...(intercepted.undos ?? []),
...afterInterceptors.undos,
],
redoMutations: [
...(intercepted.preRedos ?? []),
{ id: SetColHiddenMutation.id, params: redoMutationParams },
{ id: SetSelectionsOperation.id, params: setSelectionOperationParams },
...intercepted.redos,
...afterInterceptors.redos,
],
});
return true;
}
return false;
},
};
function getSelectedColRanges(selectionManagerService: SheetsSelectionsService): IRange[] {
const selections = selectionManagerService.getCurrentSelections();
const ranges: IRange[] = [];
if (!selections) {
return ranges;
}
for (let i = 0; i < selections.length; i++) {
const range = selections[i].range;
if (range.rangeType === RANGE_TYPE.COLUMN) {
ranges.push(range);
}
}
return ranges;
}
function getHiddenColRanges(worksheet: Worksheet, ranges: IRange[]): IRange[] {
const hiddenRanges: IRange[] = [];
for (let i = 0; i < ranges.length; i++) {
const hiddenCols = worksheet.getHiddenCols(ranges[i].startColumn, ranges[i].endColumn);
for (let j = 0; j < hiddenCols.length; j++) {
hiddenRanges.push(hiddenCols[j]);
}
}
return hiddenRanges;
}
export function divideRangesByHiddenCols(worksheet: Worksheet, ranges: IRange[]): IRange[] {
const endRow = worksheet.getRowCount() - 1;
const hiddenCols = worksheet.getHiddenCols();
const divided: IRange[] = [];
ranges.forEach((range) => {
const hiddenColsInSelection = hiddenCols.filter((c) => c.startColumn >= range.startColumn && c.endColumn <= range.endColumn);
if (hiddenColsInSelection.length) {
let startColumn = range.startColumn;
hiddenColsInSelection.forEach((hiddenRange) => {
if (hiddenRange.startColumn > startColumn) {
divided.push({ startColumn, endColumn: hiddenRange.startColumn - 1, startRow: 0, endRow });
startColumn = hiddenRange.endColumn + 1;
}
});
if (startColumn <= range.endColumn) {
divided.push({ startColumn, endColumn: range.endColumn, startRow: 0, endRow });
}
} else {
divided.push(range);
}
});
return divided;
}
/**
* Get the selections after hiding cols.
* @param worksheet the worksheet the command invoked on
* @param ranges cols to be hidden
*/
function getSelectionsAfterHiding(ranges: IRange[]): IRange[] {
const merged = mergeSelections(ranges);
// TODO@wzhudev: actually we should dedupe selections here
return merged.map((range) => {
// prefer the left selection
// we don't have to check if range.endColumn === lastColumn because this would be
// forbidden in handler of SetColHiddenCommand
const column = range.startColumn === 0 ? range.endColumn + 1 : range.startColumn - 1;
return {
...range,
startColumn: column,
endColumn: column,
};
});
}
function mergeSelections(ranges: IRange[]): IRange[] {
const merged: IRange[] = [];
let current: Nullable<IRange>;
ranges
.sort((a, b) => a.startColumn - b.startColumn)
.forEach((range) => {
if (!current) {
current = range;
return;
}
if (current.endColumn === range.startColumn - 1) {
current.endColumn = range.endColumn;
} else {
merged.push(current);
current = range;
}
});
merged.push(current!);
return merged;
}

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 type { IAccessor, ICommand } from '@univerjs/core';
import type { ISetDefinedNameMutationParam } from '@univerjs/engine-formula';
import { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
import { RemoveDefinedNameMutation, SetDefinedNameMutation, SetDefinedNameMutationFactory } from '@univerjs/engine-formula';
import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service';
/**
* The command to update defined name.
* 1. The old defined name can be obtained through IDefinedNamesService, and does not need to be passed in from the outside, making the command input more concise
* 2. Unlike InsertDefinedNameCommand, the old defined name needs to be deleted here at the same time. Because the command interception in UpdateDefinedNameController will add SetDefinedNameMutation or RemoveDefinedNameMutation, it results in that in DefinedNameController, only mutations can be listened to to update Function Description (commands cannot be listened to), so it is necessary to ensure that each mutation triggered by the command has completed all work.
*/
export const SetDefinedNameCommand: ICommand = {
id: 'sheet.command.set-defined-name',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISetDefinedNameMutationParam) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
if (!params) return false;
const newDefinedNameMutationParams: ISetDefinedNameMutationParam = {
...params,
};
const oldDefinedNameMutationParams: ISetDefinedNameMutationParam | null = SetDefinedNameMutationFactory(accessor, params);
const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: SetDefinedNameCommand.id, params });
const redos = [
...(interceptorCommands.preRedos ?? []),
...(oldDefinedNameMutationParams ? [{ id: RemoveDefinedNameMutation.id, params: oldDefinedNameMutationParams }] : []),
{ id: SetDefinedNameMutation.id, params: newDefinedNameMutationParams },
...interceptorCommands.redos,
];
const undos = [
...(interceptorCommands.preUndos ?? []),
{ id: RemoveDefinedNameMutation.id, params: newDefinedNameMutationParams },
...(oldDefinedNameMutationParams ? [{ id: SetDefinedNameMutation.id, params: oldDefinedNameMutationParams }] : []),
...interceptorCommands.undos,
];
const result = sequenceExecute(redos, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
unitID: params.unitId,
undoMutations: undos.filter(Boolean),
redoMutations: redos.filter(Boolean),
});
return true;
}
return false;
},
};

View File

@@ -0,0 +1,119 @@
/**
* 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, ICommand } from '@univerjs/core';
import type { ISetFrozenMutationParams } from '../mutations/set-frozen.mutation';
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService } from '@univerjs/core';
import { SetFrozenMutation, SetFrozenMutationFactory } from '../mutations/set-frozen.mutation';
import { getSheetCommandTarget } from './utils/target-util';
export interface ISetFrozenCommandParams {
startRow: number;
startColumn: number;
ySplit: number;
xSplit: number;
unitId?: string;
subUnitId?: string;
}
export const SetFrozenCommand: ICommand = {
type: CommandType.COMMAND,
id: 'sheet.command.set-frozen',
handler: (accessor: IAccessor, params: ISetFrozenCommandParams) => {
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService, { unitId: params.unitId, subUnitId: params.subUnitId });
if (!target) return false;
const { unitId, subUnitId, worksheet } = target;
const { startColumn, startRow, xSplit, ySplit } = params;
if (
!Number.isInteger(startRow) ||
!Number.isInteger(startColumn) ||
!Number.isInteger(xSplit) ||
!Number.isInteger(ySplit) ||
xSplit < 0 ||
ySplit < 0 ||
startRow >= worksheet.getRowCount() ||
startColumn >= worksheet.getColumnCount() ||
xSplit >= worksheet.getColumnCount() ||
ySplit >= worksheet.getRowCount()
) {
return false;
}
const redoMutationParams: ISetFrozenMutationParams = {
unitId,
subUnitId,
...params,
};
const undoMutationParams: ISetFrozenMutationParams = SetFrozenMutationFactory(accessor, redoMutationParams);
const result = commandService.syncExecuteCommand(SetFrozenMutation.id, redoMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: SetFrozenMutation.id, params: undoMutationParams }],
redoMutations: [{ id: SetFrozenMutation.id, params: redoMutationParams }],
});
return true;
}
return false;
},
};
export interface ICancelFrozenCommandParams {
unitId?: string;
subUnitId?: string;
}
export const CancelFrozenCommand: ICommand<ICancelFrozenCommandParams> = {
type: CommandType.COMMAND,
id: 'sheet.command.cancel-frozen',
handler: (accessor, params) => {
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const undoRedoService = accessor.get(IUndoRedoService);
const target = getSheetCommandTarget(univerInstanceService, { unitId: params?.unitId, subUnitId: params?.subUnitId });
if (!target) return false;
const { unitId, subUnitId } = target;
const redoMutationParams: ISetFrozenMutationParams = {
unitId,
subUnitId,
startRow: -1,
startColumn: -1,
xSplit: 0,
ySplit: 0,
};
const undoMutationParams: ISetFrozenMutationParams = SetFrozenMutationFactory(accessor, redoMutationParams);
const result = commandService.syncExecuteCommand(SetFrozenMutation.id, redoMutationParams);
if (result) {
undoRedoService.pushUndoRedo({
unitID: unitId,
undoMutations: [{ id: SetFrozenMutation.id, params: undoMutationParams }],
redoMutations: [{ id: SetFrozenMutation.id, params: redoMutationParams }],
});
return true;
}
return true;
},
};

Some files were not shown because too many files have changed in this diff Show More