snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
40
packages/sheets/README.md
Normal file
40
packages/sheets/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# @univerjs/sheets
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets)
|
||||
[](https://npmjs.com/package/@univerjs/sheets)
|
||||
[](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)
|
||||
100
packages/sheets/package.json
Normal file
100
packages/sheets/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
82
packages/sheets/src/basics/__tests__/cell-style.spec.ts
Normal file
82
packages/sheets/src/basics/__tests__/cell-style.spec.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
45
packages/sheets/src/basics/__tests__/cell-type.spec.ts
Normal file
45
packages/sheets/src/basics/__tests__/cell-type.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
32
packages/sheets/src/basics/__tests__/cell-value.spec.ts
Normal file
32
packages/sheets/src/basics/__tests__/cell-value.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
2243
packages/sheets/src/basics/__tests__/expand.data.ts
Normal file
2243
packages/sheets/src/basics/__tests__/expand.data.ts
Normal file
File diff suppressed because it is too large
Load Diff
124
packages/sheets/src/basics/__tests__/expand.spec.ts
Normal file
124
packages/sheets/src/basics/__tests__/expand.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
215
packages/sheets/src/basics/__tests__/range-merge.spec.ts
Normal file
215
packages/sheets/src/basics/__tests__/range-merge.spec.ts
Normal 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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
104
packages/sheets/src/basics/__tests__/row-column-value.spec.ts
Normal file
104
packages/sheets/src/basics/__tests__/row-column-value.spec.ts
Normal 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' } });
|
||||
});
|
||||
});
|
||||
263
packages/sheets/src/basics/__tests__/utils.spec.ts
Normal file
263
packages/sheets/src/basics/__tests__/utils.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
27
packages/sheets/src/basics/cell-position.ts
Normal file
27
packages/sheets/src/basics/cell-position.ts
Normal 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.
|
||||
}
|
||||
520
packages/sheets/src/basics/cell-style.ts
Normal file
520
packages/sheets/src/basics/cell-style.ts
Normal 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);
|
||||
}
|
||||
138
packages/sheets/src/basics/cell-type.ts
Normal file
138
packages/sheets/src/basics/cell-type.ts
Normal 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);
|
||||
}
|
||||
126
packages/sheets/src/basics/cell-value.ts
Normal file
126
packages/sheets/src/basics/cell-value.ts
Normal 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;
|
||||
}
|
||||
530
packages/sheets/src/basics/const/command-listener-const.ts
Normal file
530
packages/sheets/src/basics/const/command-listener-const.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
@@ -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: {},
|
||||
};
|
||||
18
packages/sheets/src/basics/const/index.ts
Normal file
18
packages/sheets/src/basics/const/index.ts
Normal 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';
|
||||
88
packages/sheets/src/basics/defined-name-utils.ts
Normal file
88
packages/sheets/src/basics/defined-name-utils.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { 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;
|
||||
}
|
||||
558
packages/sheets/src/basics/expand-range.ts
Normal file
558
packages/sheets/src/basics/expand-range.ts
Normal 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;
|
||||
}
|
||||
21
packages/sheets/src/basics/index.ts
Normal file
21
packages/sheets/src/basics/index.ts
Normal 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';
|
||||
24
packages/sheets/src/basics/interfaces/i-ruler-manager.ts
Normal file
24
packages/sheets/src/basics/interfaces/i-ruler-manager.ts
Normal 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;
|
||||
}
|
||||
19
packages/sheets/src/basics/interfaces/index.ts
Normal file
19
packages/sheets/src/basics/interfaces/index.ts
Normal 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';
|
||||
111
packages/sheets/src/basics/interfaces/mutation-interface.ts
Normal file
111
packages/sheets/src/basics/interfaces/mutation-interface.ts
Normal 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;
|
||||
}
|
||||
31
packages/sheets/src/basics/interfaces/selection-config.ts
Normal file
31
packages/sheets/src/basics/interfaces/selection-config.ts
Normal 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[];
|
||||
}
|
||||
21
packages/sheets/src/basics/interfaces/spreadsheet-config.ts
Normal file
21
packages/sheets/src/basics/interfaces/spreadsheet-config.ts
Normal 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;
|
||||
}
|
||||
162
packages/sheets/src/basics/range-merge.ts
Normal file
162
packages/sheets/src/basics/range-merge.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
103
packages/sheets/src/basics/row-column-value.ts
Normal file
103
packages/sheets/src/basics/row-column-value.ts
Normal 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;
|
||||
}
|
||||
212
packages/sheets/src/basics/selection.ts
Normal file
212
packages/sheets/src/basics/selection.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
29
packages/sheets/src/basics/sheet-header.ts
Normal file
29
packages/sheets/src/basics/sheet-header.ts
Normal 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;
|
||||
}
|
||||
237
packages/sheets/src/basics/split-range-text.ts
Normal file
237
packages/sheets/src/basics/split-range-text.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
259
packages/sheets/src/basics/utils.ts
Normal file
259
packages/sheets/src/basics/utils.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
@@ -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]]);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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]]);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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"}}' } },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
137
packages/sheets/src/commands/commands/append-row.command.ts
Normal file
137
packages/sheets/src/commands/commands/append-row.command.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
236
packages/sheets/src/commands/commands/auto-fill.command.ts
Normal file
236
packages/sheets/src/commands/commands/auto-fill.command.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
334
packages/sheets/src/commands/commands/copy-worksheet.command.ts
Normal file
334
packages/sheets/src/commands/commands/copy-worksheet.command.ts
Normal 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;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
692
packages/sheets/src/commands/commands/insert-row-col.command.ts
Normal file
692
packages/sheets/src/commands/commands/insert-row-col.command.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
100
packages/sheets/src/commands/commands/insert-sheet.command.ts
Normal file
100
packages/sheets/src/commands/commands/insert-sheet.command.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
349
packages/sheets/src/commands/commands/move-range.command.ts
Normal file
349
packages/sheets/src/commands/commands/move-range.command.ts
Normal 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;
|
||||
}
|
||||
366
packages/sheets/src/commands/commands/move-rows-cols.command.ts
Normal file
366
packages/sheets/src/commands/commands/move-rows-cols.command.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
33
packages/sheets/src/commands/commands/refill.command.ts
Normal file
33
packages/sheets/src/commands/commands/refill.command.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
344
packages/sheets/src/commands/commands/remove-row-col.command.ts
Normal file
344
packages/sheets/src/commands/commands/remove-row-col.command.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
109
packages/sheets/src/commands/commands/remove-sheet.command.ts
Normal file
109
packages/sheets/src/commands/commands/remove-sheet.command.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
};
|
||||
651
packages/sheets/src/commands/commands/set-border.command.ts
Normal file
651
packages/sheets/src/commands/commands/set-border.command.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
337
packages/sheets/src/commands/commands/set-col-visible.command.ts
Normal file
337
packages/sheets/src/commands/commands/set-col-visible.command.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
119
packages/sheets/src/commands/commands/set-frozen.command.ts
Normal file
119
packages/sheets/src/commands/commands/set-frozen.command.ts
Normal 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
Reference in New Issue
Block a user