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:
50
packages/sheets-table/README.md
Normal file
50
packages/sheets-table/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# @univerjs/sheets-table
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-table)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-table)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-table)
|
||||
|
||||
`@univerjs/sheets-table` adds the core table model and commands for binding structured table data in Univer Sheets.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-table` | `UniverSheetsTable` | No | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-table
|
||||
# or
|
||||
npm install @univerjs/sheets-table
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { LocaleType, mergeLocales, Univer } from '@univerjs/core';
|
||||
import { UniverSheetsTablePlugin } from '@univerjs/sheets-table';
|
||||
import EnUS from '@univerjs/sheets-table/locale/en-US';
|
||||
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(EnUS),
|
||||
},
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverSheetsTablePlugin);
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Use this package with `@univerjs/sheets-table-ui` when users need table controls in the sheet UI.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-table)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
99
packages/sheets-table/package.json
Normal file
99
packages/sheets-table/package.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-table",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Structured table model and commands 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",
|
||||
"table",
|
||||
"structured-data",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./facade": "./src/facade/index.ts",
|
||||
"./locale/*": "./src/locale/*.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/sheets": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/sheets-formula": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
104
packages/sheets-table/src/__tests__/util.spec.ts
Normal file
104
packages/sheets-table/src/__tests__/util.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 { CellValueType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SheetsTableButtonStateEnum, SheetsTableSortStateEnum, TableColumnFilterTypeEnum } from '../types/enum';
|
||||
import {
|
||||
convertCellDataToString,
|
||||
getColumnName,
|
||||
getExistingNamesSet,
|
||||
getStringFromDataStream,
|
||||
getTableFilterState,
|
||||
isConditionFilter,
|
||||
isManualTableFilter,
|
||||
} from '../util';
|
||||
|
||||
describe('sheets-table util', () => {
|
||||
it('should format column names', () => {
|
||||
expect(getColumnName(3, 'Column')).toBe('Column 3');
|
||||
});
|
||||
|
||||
it('should remove trailing CRLF from rich text stream', () => {
|
||||
expect(getStringFromDataStream({ body: { dataStream: 'A\r\n' } } as any)).toBe('A');
|
||||
expect(getStringFromDataStream({ body: { dataStream: 'B' } } as any)).toBe('B');
|
||||
});
|
||||
|
||||
it('should convert different cell value types to string', () => {
|
||||
expect(convertCellDataToString(null)).toBe('');
|
||||
expect(convertCellDataToString({ p: { body: { dataStream: 'hello\r\n' } } as any } as any)).toBe('hello');
|
||||
expect(convertCellDataToString({ t: CellValueType.STRING, v: 'x' } as any)).toBe('x');
|
||||
expect(convertCellDataToString({ t: CellValueType.FORCE_STRING, v: 1 } as any)).toBe('1');
|
||||
expect(convertCellDataToString({ t: CellValueType.BOOLEAN, v: true } as any)).toBe('TRUE');
|
||||
expect(convertCellDataToString({ t: CellValueType.BOOLEAN, v: false } as any)).toBe('FALSE');
|
||||
expect(convertCellDataToString({ t: CellValueType.NUMBER, v: 12 } as any)).toBe('12');
|
||||
expect(convertCellDataToString({ v: true } as any)).toBe('TRUE');
|
||||
expect(convertCellDataToString({ v: undefined } as any)).toBe('');
|
||||
});
|
||||
|
||||
it('should calculate filter button states from filter and sort state', () => {
|
||||
const manualFilter = { filterType: TableColumnFilterTypeEnum.manual, values: ['a'] } as any;
|
||||
|
||||
expect(getTableFilterState(manualFilter, SheetsTableSortStateEnum.None)).toBe(SheetsTableButtonStateEnum.FilteredSortNone);
|
||||
expect(getTableFilterState(manualFilter, SheetsTableSortStateEnum.Asc)).toBe(SheetsTableButtonStateEnum.FilteredSortAsc);
|
||||
expect(getTableFilterState(manualFilter, SheetsTableSortStateEnum.Desc)).toBe(SheetsTableButtonStateEnum.FilteredSortDesc);
|
||||
|
||||
expect(getTableFilterState(undefined, SheetsTableSortStateEnum.None)).toBe(SheetsTableButtonStateEnum.FilterNoneSortNone);
|
||||
expect(getTableFilterState(undefined, SheetsTableSortStateEnum.Asc)).toBe(SheetsTableButtonStateEnum.FilterNoneSortAsc);
|
||||
expect(getTableFilterState(undefined, SheetsTableSortStateEnum.Desc)).toBe(SheetsTableButtonStateEnum.FilterNoneSortDesc);
|
||||
});
|
||||
|
||||
it('should detect condition/manual filter types', () => {
|
||||
const condition = { filterType: TableColumnFilterTypeEnum.condition } as any;
|
||||
const manual = { filterType: TableColumnFilterTypeEnum.manual, values: [] } as any;
|
||||
|
||||
expect(isConditionFilter(condition)).toBe(true);
|
||||
expect(isConditionFilter(manual)).toBe(false);
|
||||
expect(isConditionFilter(undefined)).toBe(false);
|
||||
|
||||
expect(isManualTableFilter(manual)).toBe(true);
|
||||
expect(isManualTableFilter(condition)).toBe(false);
|
||||
expect(isManualTableFilter(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should collect existing names from sheets/tables/defined names as lowercase', () => {
|
||||
const workbook = {
|
||||
getSheets: () => [
|
||||
{ getName: () => 'SheetA' },
|
||||
{ getName: () => 'SheetB' },
|
||||
],
|
||||
} as any;
|
||||
|
||||
const names = getExistingNamesSet('unit-1', {
|
||||
univerInstanceService: {
|
||||
getUnit: () => workbook,
|
||||
} as any,
|
||||
tableManager: {
|
||||
getTableList: () => [
|
||||
{ name: 'Table_One' },
|
||||
{ name: 'Table_Two' },
|
||||
],
|
||||
} as any,
|
||||
definedNamesService: {
|
||||
getDefinedNameMap: () => ({
|
||||
d1: { name: 'BudgetRange' },
|
||||
}),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(names).toEqual(new Set(['sheeta', 'sheetb', 'table_one', 'table_two', 'budgetrange']));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,648 @@
|
||||
/**
|
||||
* 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 { ICommandService, IUndoRedoService, IUniverInstanceService, ObjectMatrix } from '@univerjs/core';
|
||||
import { MoveRangeMutation, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsTableController } from '../../../controllers/sheets-table.controller';
|
||||
import { TableManager } from '../../../models/table-manager';
|
||||
import {
|
||||
SheetTableInsertColCommand,
|
||||
SheetTableInsertColumnAtCommand,
|
||||
SheetTableInsertRowAtCommand,
|
||||
SheetTableInsertRowCommand,
|
||||
SheetTableRemoveColCommand,
|
||||
SheetTableRemoveColumnAtCommand,
|
||||
SheetTableRemoveRowCommand,
|
||||
} from '../sheet-table-row-col.command';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>) {
|
||||
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 any;
|
||||
}
|
||||
|
||||
interface ICreateSheetContextOptions {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
dataRange?: { endRow: number; endColumn: number };
|
||||
cells?: Array<[number, number, unknown]>;
|
||||
}
|
||||
|
||||
class TestCellMatrix {
|
||||
private readonly _matrix = new ObjectMatrix<unknown>();
|
||||
|
||||
constructor(private readonly _dataRange: { endRow: number; endColumn: number }) {}
|
||||
|
||||
getDataRange() {
|
||||
return this._dataRange;
|
||||
}
|
||||
|
||||
getValue(row: number, column: number) {
|
||||
return this._matrix.getValue(row, column);
|
||||
}
|
||||
|
||||
setValue(row: number, column: number, value: unknown) {
|
||||
this._matrix.setValue(row, column, value);
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorksheet {
|
||||
private readonly _cellMatrix: TestCellMatrix;
|
||||
|
||||
constructor(
|
||||
private readonly _sheetId: string,
|
||||
private readonly _rowCount: number,
|
||||
private readonly _columnCount: number,
|
||||
dataRange: { endRow: number; endColumn: number },
|
||||
cells: Array<[number, number, unknown]>
|
||||
) {
|
||||
this._cellMatrix = new TestCellMatrix(dataRange);
|
||||
cells.forEach(([row, column, value]) => this._cellMatrix.setValue(row, column, value));
|
||||
}
|
||||
|
||||
getSheetId() {
|
||||
return this._sheetId;
|
||||
}
|
||||
|
||||
getRowCount() {
|
||||
return this._rowCount;
|
||||
}
|
||||
|
||||
getColumnCount() {
|
||||
return this._columnCount;
|
||||
}
|
||||
|
||||
getCellMatrix() {
|
||||
return this._cellMatrix;
|
||||
}
|
||||
|
||||
getMatrixWithMergedCells() {
|
||||
return new ObjectMatrix<{ rowSpan?: number; colSpan?: number }>();
|
||||
}
|
||||
|
||||
getMergedCell() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TestWorkbook {
|
||||
constructor(
|
||||
private readonly _unitId: string,
|
||||
private readonly _worksheet: TestWorksheet
|
||||
) {}
|
||||
|
||||
getUnitId() {
|
||||
return this._unitId;
|
||||
}
|
||||
|
||||
getActiveSheet() {
|
||||
return this._worksheet;
|
||||
}
|
||||
|
||||
getSheetBySheetId(sheetId: string) {
|
||||
return sheetId === this._worksheet.getSheetId() ? this._worksheet : null;
|
||||
}
|
||||
|
||||
getStyles() {
|
||||
return {
|
||||
get: (styleId: string) => styleId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
constructor(private readonly _workbook: TestWorkbook | null = null) {}
|
||||
|
||||
getUnit(unitId: string) {
|
||||
return this._workbook?.getUnitId() === unitId ? this._workbook : null;
|
||||
}
|
||||
|
||||
getCurrentUnitOfType() {
|
||||
return this._workbook;
|
||||
}
|
||||
}
|
||||
|
||||
function createSheetContext(options: ICreateSheetContextOptions = {}) {
|
||||
const {
|
||||
unitId = 'u1',
|
||||
subUnitId = 's1',
|
||||
rowCount = 100,
|
||||
columnCount = 100,
|
||||
dataRange = { endRow: 10, endColumn: 10 },
|
||||
cells = [],
|
||||
} = options;
|
||||
const worksheet = new TestWorksheet(subUnitId, rowCount, columnCount, dataRange, cells);
|
||||
const workbook = new TestWorkbook(unitId, worksheet);
|
||||
|
||||
return {
|
||||
workbook,
|
||||
worksheet,
|
||||
univerInstanceService: new TestUniverInstanceService(workbook),
|
||||
};
|
||||
}
|
||||
|
||||
describe('sheet-table-row-col commands', () => {
|
||||
it('insert commands should return false when there is no sheet target', () => {
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, new TestUniverInstanceService()],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowCommand.handler(accessor)).toBe(false);
|
||||
expect(SheetTableInsertColCommand.handler(accessor)).toBe(false);
|
||||
});
|
||||
|
||||
it('remove commands should return false for invalid params', () => {
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, new TestUniverInstanceService()],
|
||||
]);
|
||||
|
||||
expect(SheetTableRemoveRowCommand.handler(accessor, undefined as any)).toBe(false);
|
||||
expect(SheetTableRemoveColCommand.handler(accessor, undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('commands should stop when current selection is invalid or table is missing', () => {
|
||||
const { univerInstanceService } = createSheetContext();
|
||||
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [] }],
|
||||
[SheetsTableController, { getContainerTableWithRange: () => null }],
|
||||
[TableManager, {}],
|
||||
[ICommandService, { syncExecuteCommand: vi.fn(() => true) }],
|
||||
[IUndoRedoService, { pushUndoRedo: vi.fn() }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowCommand.handler(accessor)).toBe(false);
|
||||
expect(SheetTableInsertColCommand.handler(accessor)).toBe(false);
|
||||
|
||||
expect(SheetTableRemoveRowCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' } as any)).toBe(false);
|
||||
expect(SheetTableRemoveColCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('toolbar insert row should insert worksheet rows when there is not enough trailing space', () => {
|
||||
const range = { startRow: 4, endRow: 5, startColumn: 1, endColumn: 3 };
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
rowCount: 10,
|
||||
columnCount: 20,
|
||||
dataRange: { endRow: 9, endColumn: 8 },
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range }] }],
|
||||
[SheetsTableController, { getContainerTableWithRange: () => table }],
|
||||
[TableManager, {}],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowCommand.handler(accessor)).toBe(true);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string]>).map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.insert-row',
|
||||
'sheet.mutation.set-sheet-table',
|
||||
]);
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
undoMutations: expect.arrayContaining([expect.objectContaining({ id: 'sheet.mutation.remove-rows' })]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('toolbar insert column should insert worksheet columns when there is not enough trailing space', () => {
|
||||
const range = { startRow: 1, endRow: 4, startColumn: 4, endColumn: 5 };
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
rowCount: 20,
|
||||
columnCount: 10,
|
||||
dataRange: { endRow: 8, endColumn: 9 },
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 1, endRow: 4, startColumn: 1, endColumn: 5 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range }] }],
|
||||
[SheetsTableController, { getContainerTableWithRange: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertColCommand.handler(accessor)).toBe(true);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string]>).map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.insert-col',
|
||||
'sheet.mutation.set-sheet-table',
|
||||
]);
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
undoMutations: expect.arrayContaining([expect.objectContaining({ id: 'sheet.mutation.remove-col' })]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('direct insert commands should reject invalid count, table and index inputs', () => {
|
||||
const { univerInstanceService } = createSheetContext();
|
||||
const table = {
|
||||
getSubunitId: () => 's1',
|
||||
getRange: () => ({ startRow: 1, endRow: 4, startColumn: 1, endColumn: 4 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, { getTableById: (_unitId: string, tableId: string) => (tableId === 't1' ? table : null) }],
|
||||
[ICommandService, { syncExecuteCommand: vi.fn(() => true) }],
|
||||
[IUndoRedoService, { pushUndoRedo: vi.fn() }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowAtCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1', index: 2, count: 0 })).toBe(false);
|
||||
expect(SheetTableInsertColumnAtCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 'missing', index: 2, count: 1 })).toBe(false);
|
||||
expect(SheetTableInsertRowAtCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1', index: 1, count: 1 })).toBe(false);
|
||||
expect(SheetTableInsertColumnAtCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1', index: 6, count: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('direct row insert should not push undo records when command sequence fails', () => {
|
||||
const { univerInstanceService } = createSheetContext();
|
||||
const syncExecuteCommand = vi.fn(() => false);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getSubunitId: () => 's1',
|
||||
getRange: () => ({ startRow: 0, endRow: 4, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, { getTableById: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowAtCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(false);
|
||||
expect(pushUndoRedo).not.toHaveBeenCalled();
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string]>).map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.set-sheet-table',
|
||||
]);
|
||||
});
|
||||
|
||||
it('direct row insert should update table range and move trailing table rows', () => {
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
cells: [
|
||||
[2, 1, { v: 'body-1' }],
|
||||
[10, 3, { v: 'tail' }],
|
||||
],
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getSubunitId: () => 's1',
|
||||
getRange: () => ({ startRow: 0, endRow: 4, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, { getTableById: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertRowAtCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
const calls = syncExecuteCommand.mock.calls as unknown as Array<[string, any]>;
|
||||
expect(calls.map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.set-sheet-table',
|
||||
MoveRangeMutation.id,
|
||||
]);
|
||||
expect(calls[0][1].config.updateRange.newRange).toEqual({ startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 });
|
||||
expect(calls[1][1]).toEqual(expect.objectContaining({
|
||||
fromRange: { startRow: 2, endRow: 10, startColumn: 1, endColumn: 3 },
|
||||
toRange: { startRow: 3, endRow: 11, startColumn: 1, endColumn: 3 },
|
||||
}));
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
undoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'sheet.mutation.set-sheet-table',
|
||||
params: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
updateRange: expect.objectContaining({
|
||||
newRange: { startRow: 0, endRow: 4, startColumn: 1, endColumn: 3 },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: MoveRangeMutation.id,
|
||||
params: expect.objectContaining({
|
||||
fromRange: { startRow: 3, endRow: 11, startColumn: 1, endColumn: 3 },
|
||||
toRange: { startRow: 2, endRow: 10, startColumn: 1, endColumn: 3 },
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('direct column insert should update table columns and move trailing table columns', () => {
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
cells: [
|
||||
[0, 2, { v: 'middle-col' }],
|
||||
[4, 10, { v: 'tail-col' }],
|
||||
],
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getSubunitId: () => 's1',
|
||||
getRange: () => ({ startRow: 0, endRow: 4, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, { getTableById: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableInsertColumnAtCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
const calls = syncExecuteCommand.mock.calls as unknown as Array<[string, any]>;
|
||||
expect(calls.map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.set-sheet-table',
|
||||
MoveRangeMutation.id,
|
||||
]);
|
||||
expect(calls[0][1].config.rowColOperation).toEqual({
|
||||
operationType: 'insert',
|
||||
rowColType: 'column',
|
||||
index: 2,
|
||||
count: 1,
|
||||
});
|
||||
expect(calls[1][1]).toEqual(expect.objectContaining({
|
||||
fromRange: { startRow: 0, endRow: 4, startColumn: 2, endColumn: 10 },
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 11 },
|
||||
}));
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
redoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: MoveRangeMutation.id,
|
||||
params: expect.objectContaining({
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 11 },
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('direct column remove should update table columns and move trailing table columns', () => {
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
cells: [
|
||||
[0, 3, { v: 'right-col' }],
|
||||
[4, 10, { v: 'tail-col' }],
|
||||
],
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const onCommandExecute = vi.fn(() => ({
|
||||
preRedos: [{ id: 'formula.redo.before', params: { phase: 'pre' } }],
|
||||
redos: [{ id: 'formula.redo.after', params: { phase: 'post' } }],
|
||||
preUndos: [{ id: 'formula.undo.before', params: { phase: 'pre' } }],
|
||||
undos: [{ id: 'formula.undo.after', params: { phase: 'post' } }],
|
||||
}));
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getSubunitId: () => 's1',
|
||||
getRange: () => ({ startRow: 0, endRow: 4, startColumn: 1, endColumn: 3 }),
|
||||
getTableInfo: () => ({
|
||||
name: 'Table',
|
||||
columns: [
|
||||
{ id: 'c1', displayName: '1' },
|
||||
{ id: 'c2', displayName: '2' },
|
||||
{ id: 'c3', displayName: '3' },
|
||||
],
|
||||
}),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, { getTableById: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[SheetInterceptorService, { onCommandExecute }],
|
||||
]);
|
||||
|
||||
expect(SheetTableRemoveColumnAtCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
expect(onCommandExecute).toHaveBeenCalledWith({
|
||||
id: SheetTableRemoveColumnAtCommand.id,
|
||||
params: expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
tableName: 'Table',
|
||||
removedColumnNames: ['2'],
|
||||
}),
|
||||
});
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, unknown, unknown?]>).map(([id]) => id)).toEqual([
|
||||
'formula.redo.before',
|
||||
'sheet.mutation.set-sheet-table',
|
||||
'formula.redo.after',
|
||||
MoveRangeMutation.id,
|
||||
]);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, any]>)[3][1]).toEqual(expect.objectContaining({
|
||||
fromRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 10 },
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 2, endColumn: 9 },
|
||||
}));
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
undoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'formula.undo.before' }),
|
||||
expect.objectContaining({ id: 'formula.undo.after' }),
|
||||
expect.objectContaining({
|
||||
id: MoveRangeMutation.id,
|
||||
params: expect.objectContaining({
|
||||
fromRange: { startRow: 0, endRow: 4, startColumn: 2, endColumn: 9 },
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 10 },
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
redoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'formula.redo.before' }),
|
||||
expect.objectContaining({ id: 'formula.redo.after' }),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('toolbar remove row should shrink the table and move trailing rows', () => {
|
||||
const range = { startRow: 2, endRow: 3, startColumn: 1, endColumn: 3 };
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
cells: [
|
||||
[4, 1, { v: 'after-removed-row' }],
|
||||
[10, 3, { v: 'tail-row' }],
|
||||
],
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range }] }],
|
||||
[SheetsTableController, { getContainerTableWithRange: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
expect(SheetTableRemoveRowCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' })).toBe(true);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string]>).map(([id]) => id)).toEqual([
|
||||
'sheet.mutation.set-sheet-table',
|
||||
MoveRangeMutation.id,
|
||||
]);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, any]>)[0][1].config.updateRange.newRange).toEqual({
|
||||
startRow: 0,
|
||||
endRow: 3,
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
});
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, any]>)[1][1]).toEqual(expect.objectContaining({
|
||||
fromRange: { startRow: 4, endRow: 10, startColumn: 1, endColumn: 3 },
|
||||
toRange: { startRow: 2, endRow: 8, startColumn: 1, endColumn: 3 },
|
||||
}));
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
redoMutations: expect.arrayContaining([expect.objectContaining({ id: MoveRangeMutation.id })]),
|
||||
}));
|
||||
});
|
||||
|
||||
it('toolbar remove column should notify interceptors, restore removed column metadata, and move trailing columns', () => {
|
||||
const range = { startRow: 0, endRow: 4, startColumn: 2, endColumn: 2 };
|
||||
const { univerInstanceService } = createSheetContext({
|
||||
dataRange: { endRow: 10, endColumn: 8 },
|
||||
cells: [
|
||||
[0, 3, { v: 'after-removed-col' }],
|
||||
[4, 8, { v: 'tail-col' }],
|
||||
],
|
||||
});
|
||||
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const onCommandExecute = vi.fn(() => ({
|
||||
preRedos: [{ id: 'pre-redo', params: {} }],
|
||||
redos: [{ id: 'post-redo', params: {} }],
|
||||
preUndos: [{ id: 'pre-undo', params: {} }],
|
||||
undos: [{ id: 'post-undo', params: {} }],
|
||||
}));
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 0, endRow: 4, startColumn: 1, endColumn: 4 }),
|
||||
getTableInfo: () => ({
|
||||
name: 'Table',
|
||||
columns: [
|
||||
{ id: 'c1', displayName: 'A' },
|
||||
{ id: 'c2', displayName: 'B' },
|
||||
{ id: 'c3', displayName: 'C' },
|
||||
],
|
||||
}),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, univerInstanceService],
|
||||
[TableManager, {}],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range }] }],
|
||||
[SheetsTableController, { getContainerTableWithRange: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[SheetInterceptorService, { onCommandExecute }],
|
||||
]);
|
||||
|
||||
expect(SheetTableRemoveColCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' })).toBe(true);
|
||||
expect(onCommandExecute).toHaveBeenCalledWith({
|
||||
id: SheetTableRemoveColCommand.id,
|
||||
params: expect.objectContaining({
|
||||
tableName: 'Table',
|
||||
removedColumnNames: ['B'],
|
||||
}),
|
||||
});
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string]>).map(([id]) => id)).toEqual([
|
||||
'pre-redo',
|
||||
'sheet.mutation.set-sheet-table',
|
||||
'post-redo',
|
||||
MoveRangeMutation.id,
|
||||
]);
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, any]>)[3][1]).toEqual(expect.objectContaining({
|
||||
fromRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 8 },
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 2, endColumn: 7 },
|
||||
}));
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
undoMutations: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'pre-undo' }),
|
||||
expect.objectContaining({
|
||||
id: 'sheet.mutation.set-sheet-table',
|
||||
params: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
rowColOperation: expect.objectContaining({
|
||||
columnsJson: [{ id: 'c2', displayName: 'B' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: MoveRangeMutation.id,
|
||||
params: expect.objectContaining({
|
||||
fromRange: { startRow: 0, endRow: 4, startColumn: 2, endColumn: 7 },
|
||||
toRange: { startRow: 0, endRow: 4, startColumn: 3, endColumn: 8 },
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* 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 { ITableManualFilterItem } from '../../../types/type';
|
||||
import { ICommandService, ILogService, IUndoRedoService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { AddRangeThemeMutation, RemoveRangeThemeMutation, SheetInterceptorService, SheetRangeThemeModel } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TableManager } from '../../../models/table-manager';
|
||||
import { TableColumnFilterTypeEnum } from '../../../types/enum';
|
||||
import { IRangeOperationTypeEnum, IRowColTypeEnum } from '../../../types/type';
|
||||
import { AddSheetTableMutation } from '../../mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../../mutations/delete-sheet-table.mutation';
|
||||
import { SetSheetTableMutation } from '../../mutations/set-sheet-table.mutation';
|
||||
import { SetSheetTableFilterMutation } from '../../mutations/set-table-filter.mutation';
|
||||
import { AddSheetTableCommand } from '../add-sheet-table.command';
|
||||
import { AddTableThemeCommand } from '../add-table-theme.command';
|
||||
import { DeleteSheetTableCommand } from '../delete-sheet-table.command';
|
||||
import { RemoveTableThemeCommand } from '../remove-table-theme.command';
|
||||
import { SetSheetTableCommand } from '../set-sheet-table.command';
|
||||
import { SetSheetTableFilterCommand } from '../set-table-filter.command';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>) {
|
||||
const services = new Map<unknown, unknown>(pairs);
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (!services.has(token)) {
|
||||
throw new Error(`Missing dependency: ${String(token)}`);
|
||||
}
|
||||
return services.get(token);
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('sheets-table commands', () => {
|
||||
it('AddSheetTableCommand should auto-generate table name and push undo/redo', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const tableManager = {
|
||||
getTableList: vi.fn(() => [{ name: 'table9' }]),
|
||||
getColumnHeader: vi.fn(() => ['Name']),
|
||||
};
|
||||
|
||||
const accessor = createAccessor([
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[LocaleService, { t: (key: string) => (key === 'sheets-table.tablePrefix' ? 'Table' : 'Column') }],
|
||||
[TableManager, tableManager],
|
||||
[IUniverInstanceService, {
|
||||
getUnit: () => ({ getSheets: () => [{ getName: () => 'SheetA' }] }),
|
||||
}],
|
||||
[IDefinedNamesService, {
|
||||
getDefinedNameMap: () => ({ d1: { name: 'DefinedA' } }),
|
||||
}],
|
||||
]);
|
||||
|
||||
const result = AddSheetTableCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 },
|
||||
name: 'SheetA',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(tableManager.getColumnHeader).toHaveBeenCalledWith(
|
||||
'u1',
|
||||
's1',
|
||||
{ startRow: 0, endRow: 2, startColumn: 0, endColumn: 1 },
|
||||
'Column'
|
||||
);
|
||||
expect(syncExecuteCommand).toHaveBeenCalledWith(
|
||||
AddSheetTableMutation.id,
|
||||
expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
name: 'Table10',
|
||||
header: ['Name'],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
expect(pushUndoRedo).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('AddSheetTableCommand should return false when params are missing', () => {
|
||||
expect(AddSheetTableCommand.handler(createAccessor([]), undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('DeleteSheetTableCommand should build undo mutations from table snapshot', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const onCommandExecute = vi.fn(() => ({
|
||||
preRedos: [{ id: 'formula.redo.before', params: { phase: 'pre' } }],
|
||||
redos: [{ id: 'formula.redo.after', params: { phase: 'post' } }],
|
||||
preUndos: [{ id: 'formula.undo.before', params: { phase: 'pre' } }],
|
||||
undos: [{ id: 'formula.undo.after', params: { phase: 'post' } }],
|
||||
}));
|
||||
const tableSnapshot = {
|
||||
id: 't1',
|
||||
name: 'Table1',
|
||||
range: { startRow: 1, endRow: 5, startColumn: 2, endColumn: 4 },
|
||||
options: { headerRow: true },
|
||||
};
|
||||
const tableManager = {
|
||||
getTable: vi.fn(() => ({ toJSON: () => tableSnapshot })),
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[TableManager, tableManager],
|
||||
[ILogService, { error: vi.fn() }],
|
||||
[SheetInterceptorService, { onCommandExecute }],
|
||||
]);
|
||||
|
||||
const result = DeleteSheetTableCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(onCommandExecute).toHaveBeenCalledWith({
|
||||
id: DeleteSheetTableCommand.id,
|
||||
params: {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
tableName: 'Table1',
|
||||
},
|
||||
});
|
||||
expect((syncExecuteCommand.mock.calls as unknown as Array<[string, unknown, unknown?]>).map(([id]) => id)).toEqual([
|
||||
'formula.redo.before',
|
||||
DeleteSheetTableMutation.id,
|
||||
'formula.redo.after',
|
||||
]);
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitID: 'u1',
|
||||
undoMutations: [
|
||||
expect.objectContaining({ id: 'formula.undo.before' }),
|
||||
expect.objectContaining({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: expect.objectContaining({
|
||||
tableId: 't1',
|
||||
name: 'Table1',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({ id: 'formula.undo.after' }),
|
||||
],
|
||||
redoMutations: [
|
||||
expect.objectContaining({ id: 'formula.redo.before' }),
|
||||
expect.objectContaining({ id: DeleteSheetTableMutation.id }),
|
||||
expect.objectContaining({ id: 'formula.redo.after' }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('DeleteSheetTableCommand should return false when table does not exist', () => {
|
||||
const logService = { error: vi.fn() };
|
||||
const accessor = createAccessor([
|
||||
[IUndoRedoService, { pushUndoRedo: vi.fn() }],
|
||||
[ICommandService, { syncExecuteCommand: vi.fn(() => true) }],
|
||||
[TableManager, { getTable: () => undefined }],
|
||||
[ILogService, logService],
|
||||
]);
|
||||
|
||||
expect(DeleteSheetTableCommand.handler(accessor, { unitId: 'u1', subUnitId: 's1', tableId: 't1' })).toBe(false);
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SetSheetTableCommand should reject invalid table name and warn', () => {
|
||||
const logService = { warn: vi.fn() };
|
||||
const commandService = { executeCommand: vi.fn() };
|
||||
const table = {
|
||||
getDisplayName: () => 'Old',
|
||||
getRange: () => ({ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 }),
|
||||
getSubunitId: () => 's1',
|
||||
getTableStyleId: () => 'theme-1',
|
||||
};
|
||||
|
||||
const accessor = createAccessor([
|
||||
[TableManager, { getTableById: () => table, getTableList: () => [] }],
|
||||
[LocaleService, { t: () => 'invalid name' }],
|
||||
[IUniverInstanceService, {
|
||||
getUnit: () => ({ getSheets: () => [{ getName: () => 'SheetA' }] }),
|
||||
}],
|
||||
[IDefinedNamesService, { getDefinedNameMap: () => ({}) }],
|
||||
[ICommandService, commandService],
|
||||
[IUndoRedoService, { pushUndoRedo: vi.fn() }],
|
||||
[ILogService, logService],
|
||||
]);
|
||||
|
||||
const result = SetSheetTableCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
name: 'SheetA',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logService.warn).toHaveBeenCalledWith('invalid name');
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SetSheetTableCommand should execute mutation and build inverse undo config', () => {
|
||||
const executeCommand = vi.fn();
|
||||
const pushUndoRedo = vi.fn();
|
||||
const onCommandExecute = vi.fn(() => ({
|
||||
preRedos: [{ id: 'formula.redo.before', params: { phase: 'pre' } }],
|
||||
redos: [{ id: 'formula.redo.after', params: { phase: 'post' } }],
|
||||
preUndos: [{ id: 'formula.undo.before', params: { phase: 'pre' } }],
|
||||
undos: [{ id: 'formula.undo.after', params: { phase: 'post' } }],
|
||||
}));
|
||||
const table = {
|
||||
getDisplayName: () => 'OldName',
|
||||
getRange: () => ({ startRow: 1, endRow: 4, startColumn: 1, endColumn: 3 }),
|
||||
getSubunitId: () => 's1',
|
||||
getTableStyleId: () => 'theme-old',
|
||||
};
|
||||
|
||||
const accessor = createAccessor([
|
||||
[TableManager, { getTableById: () => table, getTableList: () => [] }],
|
||||
[LocaleService, { t: () => 'msg' }],
|
||||
[IUniverInstanceService, { getUnit: () => ({ getSheets: () => [{ getName: () => 'SheetA' }] }) }],
|
||||
[IDefinedNamesService, { getDefinedNameMap: () => ({}) }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[ILogService, { warn: vi.fn() }],
|
||||
[SheetInterceptorService, { onCommandExecute }],
|
||||
]);
|
||||
|
||||
const result = SetSheetTableCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
name: 'NewName',
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: 2,
|
||||
count: 1,
|
||||
},
|
||||
updateRange: {
|
||||
newRange: { startRow: 1, endRow: 6, startColumn: 1, endColumn: 4 },
|
||||
},
|
||||
theme: 'theme-new',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(onCommandExecute).toHaveBeenCalledWith({
|
||||
id: SetSheetTableCommand.id,
|
||||
params: expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
name: 'NewName',
|
||||
oldTableName: 'OldName',
|
||||
}),
|
||||
});
|
||||
expect(executeCommand.mock.calls.map(([id]) => id)).toEqual([
|
||||
'formula.redo.before',
|
||||
SetSheetTableMutation.id,
|
||||
'formula.redo.after',
|
||||
]);
|
||||
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
undoMutations: [
|
||||
expect.objectContaining({ id: 'formula.undo.before' }),
|
||||
expect.objectContaining({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
name: 'OldName',
|
||||
theme: 'theme-old',
|
||||
rowColOperation: expect.objectContaining({ operationType: IRangeOperationTypeEnum.Delete }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({ id: 'formula.undo.after' }),
|
||||
],
|
||||
redoMutations: [
|
||||
expect.objectContaining({ id: 'formula.redo.before' }),
|
||||
expect.objectContaining({ id: SetSheetTableMutation.id }),
|
||||
expect.objectContaining({ id: 'formula.redo.after' }),
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('SetSheetTableFilterCommand should restore the previous filter on undo', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const previousFilter: ITableManualFilterItem = {
|
||||
filterType: TableColumnFilterTypeEnum.manual,
|
||||
values: ['Old'],
|
||||
};
|
||||
const accessor = createAccessor([
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[TableManager, {
|
||||
getTable: () => ({
|
||||
getTableFilterColumn: () => previousFilter,
|
||||
}),
|
||||
}],
|
||||
]);
|
||||
|
||||
const result = SetSheetTableFilterCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
column: 1,
|
||||
tableFilter: { filterType: TableColumnFilterTypeEnum.manual, values: ['A'] },
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(syncExecuteCommand).toHaveBeenCalledWith(
|
||||
SetSheetTableFilterMutation.id,
|
||||
expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
column: 1,
|
||||
tableFilter: { filterType: TableColumnFilterTypeEnum.manual, values: ['A'] },
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
expect(pushUndoRedo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
undoMutations: [{
|
||||
id: SetSheetTableFilterMutation.id,
|
||||
params: {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
column: 1,
|
||||
tableFilter: previousFilter,
|
||||
},
|
||||
}],
|
||||
}));
|
||||
});
|
||||
|
||||
it('AddTableThemeCommand should register theme and update table style', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
const themeStyle = {
|
||||
toJson: () => ({ name: 'theme-new' }),
|
||||
getName: () => 'theme-new',
|
||||
} as any;
|
||||
const table = {
|
||||
getSubunitId: () => 's1',
|
||||
getTableStyleId: () => 'theme-old',
|
||||
};
|
||||
|
||||
const accessor = createAccessor([
|
||||
[TableManager, { getTableById: () => table }],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
const result = AddTableThemeCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
themeStyle,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(syncExecuteCommand).toHaveBeenNthCalledWith(1, AddRangeThemeMutation.id, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
styleJSON: { name: 'theme-new' },
|
||||
}, undefined);
|
||||
expect(syncExecuteCommand).toHaveBeenNthCalledWith(2, SetSheetTableMutation.id, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
config: { theme: 'theme-new' },
|
||||
}, undefined);
|
||||
expect(pushUndoRedo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RemoveTableThemeCommand should select replacement theme and build undo payload', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const pushUndoRedo = vi.fn();
|
||||
|
||||
const accessor = createAccessor([
|
||||
[TableManager, { getTableById: () => ({ getSubunitId: () => 's1' }) }],
|
||||
[SheetRangeThemeModel, {
|
||||
getRegisteredRangeThemes: () => ['table-default-0', 'table-default-1', 'table-custom-1', 'table-custom-2'],
|
||||
getDefaultRangeThemeStyle: () => ({ toJson: () => ({ name: 'table-custom-1' }) }),
|
||||
}],
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUndoRedoService, { pushUndoRedo }],
|
||||
]);
|
||||
|
||||
const result = RemoveTableThemeCommand.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
themeName: 'table-custom-1',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(syncExecuteCommand).toHaveBeenNthCalledWith(1, SetSheetTableMutation.id, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
config: { theme: 'table-custom-2' },
|
||||
}, undefined);
|
||||
expect(syncExecuteCommand).toHaveBeenNthCalledWith(2, RemoveRangeThemeMutation.id, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
styleName: 'table-custom-1',
|
||||
}, undefined);
|
||||
expect(pushUndoRedo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { validateSheetTableName } from '../../../util/table-name';
|
||||
|
||||
describe('validateSheetTableName', () => {
|
||||
it('accepts a unique custom name', () => {
|
||||
expect(validateSheetTableName('SalesTable', new Set(['OtherTable']))).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('rejects duplicate names case-insensitively', () => {
|
||||
expect(validateSheetTableName('SalesTable', new Set(['salestable']))).toEqual({ valid: false, reason: 'invalid' });
|
||||
});
|
||||
|
||||
it('rejects empty names, spaces, and leading numbers', () => {
|
||||
expect(validateSheetTableName('', new Set())).toEqual({ valid: false, reason: 'empty' });
|
||||
expect(validateSheetTableName('Sales Table', new Set())).toEqual({ valid: false, reason: 'invalid' });
|
||||
expect(validateSheetTableName('1SalesTable', new Set())).toEqual({ valid: false, reason: 'invalid' });
|
||||
});
|
||||
});
|
||||
@@ -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 { ICommand, IMutationInfo, IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { ITableOptions } from '../../types/type';
|
||||
import {
|
||||
CommandType,
|
||||
customNameCharacterCheck,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { getExistingNamesSet } from '../../util';
|
||||
import { AddSheetTableMutation } from '../mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../mutations/delete-sheet-table.mutation';
|
||||
|
||||
export interface IAddSheetTableCommandParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
range: IRange;
|
||||
id?: string;
|
||||
name?: string;
|
||||
options?: ITableOptions;
|
||||
}
|
||||
|
||||
export const AddSheetTableCommand: ICommand<IAddSheetTableCommandParams> = {
|
||||
id: 'sheet.command.add-table',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const tableManager = accessor.get(TableManager);
|
||||
const tableId = params.id ?? generateRandomId();
|
||||
const existingNamesSet = getExistingNamesSet(params.unitId, {
|
||||
univerInstanceService: accessor.get(IUniverInstanceService),
|
||||
tableManager,
|
||||
definedNamesService: accessor.get(IDefinedNamesService),
|
||||
});
|
||||
|
||||
let tableName = params.name;
|
||||
if (!tableName || !customNameCharacterCheck(tableName.toLowerCase(), existingNamesSet)) {
|
||||
const prefix = localeService.t<LocaleKey>('sheets-table.tablePrefix');
|
||||
let index = tableManager.getTableList(params.unitId).length + 1;
|
||||
|
||||
for (const name of existingNamesSet) {
|
||||
if (name.startsWith(prefix.toLowerCase())) {
|
||||
const n = Number(name.slice(prefix.length));
|
||||
if (Number.isInteger(n) && n >= index) {
|
||||
index = n + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tableName = `${prefix}${index}`;
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const { unitId, subUnitId, range } = params;
|
||||
const header = tableManager.getColumnHeader(unitId, subUnitId, range, localeService.t<LocaleKey>('sheets-table.columnPrefix'));
|
||||
|
||||
redos.push({ id: AddSheetTableMutation.id, params: { ...params, tableId, name: tableName, header } });
|
||||
undos.push({ id: DeleteSheetTableMutation.id, params: { tableId, unitId: params.unitId } });
|
||||
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
|
||||
if (res.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: params.unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { RangeThemeStyle } from '@univerjs/sheets';
|
||||
import { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
|
||||
import { AddRangeThemeMutation, RemoveRangeThemeMutation } from '@univerjs/sheets';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { SetSheetTableMutation } from '../mutations/set-sheet-table.mutation';
|
||||
|
||||
export interface IAddTableThemeCommandParams {
|
||||
unitId: string;
|
||||
tableId: string;
|
||||
themeStyle: RangeThemeStyle;
|
||||
}
|
||||
|
||||
export const AddTableThemeCommand: ICommand<IAddTableThemeCommandParams> = {
|
||||
id: 'sheet.command.add-table-theme',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableManager = accessor.get(TableManager);
|
||||
|
||||
const { unitId, tableId, themeStyle } = params;
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const table = tableManager.getTableById(unitId, tableId);
|
||||
if (!table) return false;
|
||||
const subUnitId = table.getSubunitId();
|
||||
|
||||
redos.push({
|
||||
id: AddRangeThemeMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
styleJSON: themeStyle.toJson(),
|
||||
},
|
||||
});
|
||||
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
theme: themeStyle.getName(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
themeStyle: table.getTableStyleId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: RemoveRangeThemeMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
styleName: themeStyle.getName(),
|
||||
},
|
||||
});
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -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 { ICommand, IMutationInfo } from '@univerjs/core';
|
||||
import type { IDeleteSheetTableParams } from '../mutations/delete-sheet-table.mutation';
|
||||
import { CommandType, ICommandService, ILogService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { AddSheetTableMutation } from '../mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../mutations/delete-sheet-table.mutation';
|
||||
|
||||
export const DeleteSheetTableCommand: ICommand<IDeleteSheetTableParams> = {
|
||||
id: 'sheet.command.delete-table',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const sheetTableManager = accessor.get(TableManager);
|
||||
const logService = accessor.get(ILogService);
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const tableInstance = sheetTableManager.getTable(params.unitId, params.tableId);
|
||||
const tableConfig = tableInstance?.toJSON();
|
||||
|
||||
if (!tableConfig) {
|
||||
logService.error('[TableManager]: Table not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const interceptorCommands = sheetInterceptorService.onCommandExecute({
|
||||
id: DeleteSheetTableCommand.id,
|
||||
params: {
|
||||
...params,
|
||||
tableName: tableConfig.name,
|
||||
},
|
||||
});
|
||||
|
||||
redos.push(...(interceptorCommands.preRedos ?? []));
|
||||
redos.push({ id: DeleteSheetTableMutation.id, params: { ...params } });
|
||||
redos.push(...interceptorCommands.redos);
|
||||
undos.push(...(interceptorCommands.preUndos ?? []));
|
||||
undos.push({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId: params.unitId,
|
||||
subUnitId: params.subUnitId,
|
||||
tableId: params.tableId,
|
||||
name: tableConfig.name,
|
||||
range: tableConfig.range,
|
||||
options: tableConfig.options,
|
||||
},
|
||||
});
|
||||
undos.push(...interceptorCommands.undos);
|
||||
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
|
||||
if (res.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: params.unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
|
||||
import { AddRangeThemeMutation, RemoveRangeThemeMutation, SheetRangeThemeModel } from '@univerjs/sheets';
|
||||
import { SHEET_TABLE_CUSTOM_THEME_PREFIX } from '../../const';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { SetSheetTableMutation } from '../mutations/set-sheet-table.mutation';
|
||||
|
||||
export interface IRemoveTableThemeCommandParams {
|
||||
unitId: string;
|
||||
tableId: string;
|
||||
themeName: string;
|
||||
}
|
||||
|
||||
export const RemoveTableThemeCommand: ICommand<IRemoveTableThemeCommandParams> = {
|
||||
id: 'sheet.command.remove-table-theme',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, tableId, themeName } = params;
|
||||
|
||||
const tableManager = accessor.get(TableManager);
|
||||
const rangeThemeModel = accessor.get(SheetRangeThemeModel);
|
||||
const table = tableManager.getTableById(unitId, tableId);
|
||||
if (!table) return false;
|
||||
const subUnitId = table.getSubunitId();
|
||||
|
||||
const redos = [];
|
||||
const undos = [];
|
||||
|
||||
const defaultRangeThemes = rangeThemeModel.getRegisteredRangeThemes().filter((item) => item?.startsWith('table-default'));
|
||||
const customRangeThemes = rangeThemeModel.getRegisteredRangeThemes().filter((item) => item?.startsWith(SHEET_TABLE_CUSTOM_THEME_PREFIX));
|
||||
|
||||
let shouldBeSelectedTheme = customRangeThemes.find((item) => item !== themeName);
|
||||
if (!shouldBeSelectedTheme) {
|
||||
shouldBeSelectedTheme = defaultRangeThemes[0];
|
||||
}
|
||||
|
||||
redos.push({ id: SetSheetTableMutation.id, params: { unitId, subUnitId, tableId, config: { theme: shouldBeSelectedTheme } } });
|
||||
redos.push({ id: RemoveRangeThemeMutation.id, params: { unitId, subUnitId, styleName: themeName } });
|
||||
|
||||
const themeStyle = rangeThemeModel.getDefaultRangeThemeStyle(themeName);
|
||||
if (themeStyle) {
|
||||
undos.push({ id: AddRangeThemeMutation.id, params: { unitId, subUnitId, styleJSON: themeStyle.toJson() } });
|
||||
undos.push({ id: SetSheetTableMutation.id, params: { unitId, subUnitId, tableId, config: { theme: themeName } } });
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: redos,
|
||||
undoMutations: undos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import type { ITableSetConfig } from '../../types/type';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
} from '@univerjs/core';
|
||||
import { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { IRangeOperationTypeEnum } from '../../types/type';
|
||||
import { getExistingNamesSet } from '../../util';
|
||||
import { validateSheetTableName } from '../../util/table-name';
|
||||
import { SetSheetTableMutation } from '../mutations/set-sheet-table.mutation';
|
||||
|
||||
export interface ISetSheetTableCommandParams extends ITableSetConfig {
|
||||
unitId: string;
|
||||
tableId: string;
|
||||
oldTableName?: string;
|
||||
}
|
||||
|
||||
export const SetSheetTableCommand: ICommand<ISetSheetTableCommandParams> = {
|
||||
id: 'sheet.command.set-table-config',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, tableId, name, updateRange, rowColOperation, theme } = params;
|
||||
const tableManager = accessor.get(TableManager);
|
||||
const table = tableManager.getTableById(unitId, tableId);
|
||||
|
||||
if (!table) return false;
|
||||
|
||||
const oldTableConfig: ITableSetConfig = {};
|
||||
const newTableConfig: ITableSetConfig = {};
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const existingNamesSet = getExistingNamesSet(unitId, {
|
||||
univerInstanceService: accessor.get(IUniverInstanceService),
|
||||
tableManager,
|
||||
definedNamesService: accessor.get(IDefinedNamesService),
|
||||
});
|
||||
|
||||
if (name) {
|
||||
const tableNameValidation = validateSheetTableName(name, existingNamesSet);
|
||||
if (!tableNameValidation.valid) {
|
||||
const logService = accessor.get(ILogService);
|
||||
logService.warn(localeService.t<LocaleKey>('sheets-table.tableNameError'));
|
||||
return false;
|
||||
}
|
||||
|
||||
oldTableConfig.name = table.getDisplayName();
|
||||
newTableConfig.name = name;
|
||||
}
|
||||
|
||||
if (rowColOperation) {
|
||||
oldTableConfig.rowColOperation = {
|
||||
operationType: rowColOperation.operationType === IRangeOperationTypeEnum.Insert ? IRangeOperationTypeEnum.Delete : IRangeOperationTypeEnum.Insert,
|
||||
rowColType: rowColOperation.rowColType,
|
||||
index: rowColOperation.index,
|
||||
count: rowColOperation.count,
|
||||
};
|
||||
newTableConfig.rowColOperation = rowColOperation;
|
||||
}
|
||||
|
||||
if (updateRange) {
|
||||
oldTableConfig.updateRange = {
|
||||
newRange: table.getRange(),
|
||||
};
|
||||
newTableConfig.updateRange = updateRange;
|
||||
}
|
||||
|
||||
if (theme) {
|
||||
oldTableConfig.theme = table.getTableStyleId();
|
||||
newTableConfig.theme = theme;
|
||||
}
|
||||
|
||||
const redoParams = {
|
||||
unitId,
|
||||
subUnitId: table.getSubunitId(),
|
||||
tableId,
|
||||
config: newTableConfig,
|
||||
};
|
||||
const undoParams = {
|
||||
unitId,
|
||||
subUnitId: table.getSubunitId(),
|
||||
tableId,
|
||||
config: oldTableConfig,
|
||||
};
|
||||
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const interceptorCommands = sheetInterceptorService.onCommandExecute({
|
||||
id: SetSheetTableCommand.id,
|
||||
params: {
|
||||
...params,
|
||||
oldTableName: oldTableConfig.name,
|
||||
},
|
||||
});
|
||||
const redos = [
|
||||
...(interceptorCommands.preRedos ?? []),
|
||||
{ id: SetSheetTableMutation.id, params: redoParams },
|
||||
...interceptorCommands.redos,
|
||||
];
|
||||
const undos = [
|
||||
...(interceptorCommands.preUndos ?? []),
|
||||
{ id: SetSheetTableMutation.id, params: undoParams },
|
||||
...interceptorCommands.undos,
|
||||
];
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
redos.forEach((mutation) => {
|
||||
commandService.executeCommand(mutation.id, mutation.params);
|
||||
});
|
||||
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return 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, IMutationInfo } from '@univerjs/core';
|
||||
import type { ISetSheetTableParams } from '../mutations/set-table-filter.mutation';
|
||||
import { CommandType, generateRandomId, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { SetSheetTableFilterMutation } from '../mutations/set-table-filter.mutation';
|
||||
|
||||
export const SetSheetTableFilterCommand: ICommand<ISetSheetTableParams> = {
|
||||
id: 'sheet.command.set-table-filter',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const tableManager = accessor.get(TableManager);
|
||||
const tableId = params.tableId || generateRandomId();
|
||||
const previousFilter = tableManager.getTable(params.unitId, tableId)?.getTableFilterColumn(params.column);
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
redos.push({ id: SetSheetTableFilterMutation.id, params: { ...params, tableId } });
|
||||
undos.push({ id: SetSheetTableFilterMutation.id, params: { ...params, tableId, tableFilter: previousFilter } });
|
||||
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
|
||||
if (res.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: params.unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { ITableFilterJSON } from '../../types/type';
|
||||
import { CommandType, ICommandService, IUndoRedoService } from '@univerjs/core';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { SetSheetTableMutation } from '../mutations/set-sheet-table.mutation';
|
||||
|
||||
export interface ISetSheetTableSortStateParams {
|
||||
unitId: string;
|
||||
tableId: string;
|
||||
sortInfo: ITableFilterJSON['tableSortInfo'];
|
||||
}
|
||||
|
||||
export const SetSheetTableSortStateCommand: ICommand<ISetSheetTableSortStateParams> = {
|
||||
id: 'sheet.command.set-table-sort-state',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const table = accessor.get(TableManager).getTable(params.unitId, params.tableId);
|
||||
if (!table) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousSortInfo = table.getTableFilters().toJSON().tableSortInfo;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const mutationParams = {
|
||||
unitId: params.unitId,
|
||||
subUnitId: table.getSubunitId(),
|
||||
tableId: params.tableId,
|
||||
config: { sortInfo: params.sortInfo },
|
||||
};
|
||||
const success = commandService.syncExecuteCommand(SetSheetTableMutation.id, mutationParams);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
accessor.get(IUndoRedoService).pushUndoRedo({
|
||||
unitID: params.unitId,
|
||||
undoMutations: [{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: { ...mutationParams, config: { sortInfo: previousSortInfo } },
|
||||
}],
|
||||
redoMutations: [{ id: SetSheetTableMutation.id, params: mutationParams }],
|
||||
});
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,939 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-lines-per-function */
|
||||
|
||||
import type { IAccessor, ICommand, IMutationInfo } from '@univerjs/core';
|
||||
import type { ITableColumnJson } from '../../types/type';
|
||||
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute } from '@univerjs/core';
|
||||
import { getMoveRangeUndoRedoMutations, getSheetCommandTarget, InsertColMutation, InsertRowMutation, RemoveColMutation, RemoveRowMutation, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetsTableController } from '../../controllers/sheets-table.controller';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { IRangeOperationTypeEnum, IRowColTypeEnum } from '../../types/type';
|
||||
import { SetSheetTableMutation } from '../mutations/set-sheet-table.mutation';
|
||||
|
||||
interface ISheetTableRowColOperationCommandParams {
|
||||
tableId: string;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
interface ISheetTableInsertAtCommandParams extends ISheetTableRowColOperationCommandParams {
|
||||
index: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
function executeTableMutationSequence(
|
||||
accessor: IAccessor,
|
||||
unitId: string,
|
||||
redos: IMutationInfo[],
|
||||
undos: IMutationInfo[]
|
||||
): boolean {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const SheetTableInsertRowCommand: ICommand<ISheetTableRowColOperationCommandParams> = {
|
||||
id: 'sheet.command.table-insert-row',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { workbook, worksheet, unitId, subUnitId } = target;
|
||||
|
||||
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
|
||||
const selections = sheetsSelectionsService.getCurrentSelections();
|
||||
if (!selections.length || selections.length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableManager = accessor.get(TableManager);
|
||||
|
||||
const selection = selections[0];
|
||||
const range = selection.range;
|
||||
|
||||
const sheetsTableController = accessor.get(SheetsTableController);
|
||||
const table = sheetsTableController.getContainerTableWithRange(unitId, subUnitId, range);
|
||||
|
||||
if (!table) return false;
|
||||
|
||||
const insertRowCount = range.endRow - range.startRow + 1;
|
||||
|
||||
const worksheetCount = worksheet.getRowCount();
|
||||
const worksheetLastRowIndex = worksheetCount - 1;
|
||||
const rowContentIndex = worksheet.getCellMatrix().getDataRange().endRow;
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
if (worksheetLastRowIndex - rowContentIndex < insertRowCount) {
|
||||
// should insert row
|
||||
redos.push({
|
||||
id: InsertRowMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
},
|
||||
});
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...table.getRange(),
|
||||
endRow: table.getRange().endRow + insertRowCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: table.getRange(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveRowMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const oldRange = { ...table.getRange() };
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...oldRange,
|
||||
endRow: oldRange.endRow + insertRowCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: { ...oldRange },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: range.startRow,
|
||||
endRow: rowContentIndex,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: range.startRow + insertRowCount,
|
||||
endRow: rowContentIndex + insertRowCount,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableInsertRowAtCommand: ICommand<ISheetTableInsertAtCommandParams> = {
|
||||
id: 'sheet.command.table-insert-row-at',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, tableId, index, count = 1 } = params;
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const table = accessor.get(TableManager).getTableById(unitId, tableId);
|
||||
if (!table || table.getSubunitId() !== subUnitId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldRange = table.getRange();
|
||||
if (index <= oldRange.startRow || index > oldRange.endRow + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...oldRange,
|
||||
endRow: oldRange.endRow + count,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
const undos: IMutationInfo[] = [{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: { ...oldRange },
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
const rowContentIndex = target.worksheet.getCellMatrix().getDataRange().endRow;
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: index,
|
||||
endRow: rowContentIndex,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: index + count,
|
||||
endRow: rowContentIndex + count,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
|
||||
return executeTableMutationSequence(accessor, unitId, redos, undos);
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableInsertColCommand: ICommand<ISheetTableRowColOperationCommandParams> = {
|
||||
id: 'sheet.command.table-insert-col',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
|
||||
const selections = sheetsSelectionsService.getCurrentSelections();
|
||||
if (!selections.length || selections.length > 1) {
|
||||
return false;
|
||||
}
|
||||
const selection = selections[0];
|
||||
const range = selection.range;
|
||||
const sheetsTableController = accessor.get(SheetsTableController);
|
||||
const table = sheetsTableController.getContainerTableWithRange(unitId, subUnitId, range);
|
||||
if (!table) return false;
|
||||
const insertColCount = range.endColumn - range.startColumn + 1;
|
||||
const worksheetCount = worksheet.getColumnCount();
|
||||
const worksheetLastColIndex = worksheetCount - 1;
|
||||
const colContentIndex = worksheet.getCellMatrix().getDataRange().endColumn;
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
if (worksheetLastColIndex - colContentIndex < insertColCount) {
|
||||
// should insert col
|
||||
redos.push({
|
||||
id: InsertColMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
},
|
||||
});
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveColMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const oldRange = table.getRange();
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: range.startColumn,
|
||||
endColumn: colContentIndex,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: range.startColumn + insertColCount,
|
||||
endColumn: colContentIndex + insertColCount,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableInsertColumnAtCommand: ICommand<ISheetTableInsertAtCommandParams> = {
|
||||
id: 'sheet.command.table-insert-column-at',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, tableId, index, count = 1 } = params;
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const table = accessor.get(TableManager).getTableById(unitId, tableId);
|
||||
if (!table || table.getSubunitId() !== subUnitId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldRange = table.getRange();
|
||||
if (index < oldRange.startColumn || index > oldRange.endColumn + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index,
|
||||
count,
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
const undos: IMutationInfo[] = [{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index,
|
||||
count,
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
const colContentIndex = target.worksheet.getCellMatrix().getDataRange().endColumn;
|
||||
if (index <= colContentIndex) {
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: index,
|
||||
endColumn: colContentIndex,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: index + count,
|
||||
endColumn: colContentIndex + count,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
}
|
||||
|
||||
return executeTableMutationSequence(accessor, unitId, redos, undos);
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableRemoveColumnAtCommand: ICommand<ISheetTableInsertAtCommandParams> = {
|
||||
id: 'sheet.command.table-remove-column-at',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, tableId, index, count = 1 } = params;
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const table = accessor.get(TableManager).getTableById(unitId, tableId);
|
||||
if (!table || table.getSubunitId() !== subUnitId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldRange = table.getRange();
|
||||
if (index < oldRange.startColumn || index + count - 1 > oldRange.endColumn || count >= oldRange.endColumn - oldRange.startColumn + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableInfo = table.getTableInfo();
|
||||
const columns: ITableColumnJson[] = [];
|
||||
const gap = index - oldRange.startColumn;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const column = tableInfo.columns[gap + i];
|
||||
if (column) {
|
||||
columns.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const interceptorCommands = sheetInterceptorService.onCommandExecute({
|
||||
id: SheetTableRemoveColumnAtCommand.id,
|
||||
params: {
|
||||
...params,
|
||||
tableName: tableInfo.name,
|
||||
removedColumnNames: columns.map((column) => column.displayName),
|
||||
},
|
||||
});
|
||||
|
||||
const redos: IMutationInfo[] = [
|
||||
...(interceptorCommands.preRedos ?? []),
|
||||
{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index,
|
||||
count,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...interceptorCommands.redos,
|
||||
];
|
||||
const undos: IMutationInfo[] = [
|
||||
...(interceptorCommands.preUndos ?? []),
|
||||
{
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index,
|
||||
count,
|
||||
columnsJson: columns,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...interceptorCommands.undos,
|
||||
];
|
||||
|
||||
const colContentIndex = target.worksheet.getCellMatrix().getDataRange().endColumn;
|
||||
if (index + count <= colContentIndex) {
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: index + count,
|
||||
endColumn: colContentIndex,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: index,
|
||||
endColumn: colContentIndex - count,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
}
|
||||
|
||||
return executeTableMutationSequence(accessor, unitId, redos, undos);
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableRemoveRowCommand: ICommand<ISheetTableRowColOperationCommandParams> = {
|
||||
id: 'sheet.command.table-remove-row',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId } = target;
|
||||
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
|
||||
const selections = sheetsSelectionsService.getCurrentSelections();
|
||||
if (!selections.length || selections.length > 1) {
|
||||
return false;
|
||||
}
|
||||
const selection = selections[0];
|
||||
const range = selection.range;
|
||||
const sheetsTableController = accessor.get(SheetsTableController);
|
||||
const table = sheetsTableController.getContainerTableWithRange(unitId, subUnitId, range);
|
||||
if (!table) return false;
|
||||
const removeRowCount = range.endRow - range.startRow + 1;
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const oldRange = table.getRange();
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...oldRange,
|
||||
endRow: oldRange.endRow - removeRowCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: { ...oldRange },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const worksheet = target.worksheet;
|
||||
const rowContentIndex = worksheet.getCellMatrix().getDataRange().endRow;
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: range.endRow + 1,
|
||||
endRow: rowContentIndex,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: range.startRow,
|
||||
endRow: rowContentIndex - removeRowCount,
|
||||
startColumn: oldRange.startColumn,
|
||||
endColumn: oldRange.endColumn,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetTableRemoveColCommand: ICommand<ISheetTableRowColOperationCommandParams> = {
|
||||
id: 'sheet.command.table-remove-col',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const { workbook, unitId, subUnitId } = target;
|
||||
const tableManager = accessor.get(TableManager);
|
||||
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
|
||||
const selections = sheetsSelectionsService.getCurrentSelections();
|
||||
if (!selections.length || selections.length > 1) {
|
||||
return false;
|
||||
}
|
||||
const selection = selections[0];
|
||||
const range = selection.range;
|
||||
const sheetsTableController = accessor.get(SheetsTableController);
|
||||
const table = sheetsTableController.getContainerTableWithRange(unitId, subUnitId, range);
|
||||
if (!table) return false;
|
||||
const removeColCount = range.endColumn - range.startColumn + 1;
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const oldRange = table.getRange();
|
||||
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: removeColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tableInfo = table.getTableInfo();
|
||||
const columns: ITableColumnJson[] = [];
|
||||
const gap = range.startColumn - oldRange.startColumn;
|
||||
for (let i = 0; i < removeColCount; i++) {
|
||||
const column = tableInfo.columns[gap + i];
|
||||
if (column) {
|
||||
columns.push(column);
|
||||
}
|
||||
}
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const interceptorCommands = sheetInterceptorService.onCommandExecute({
|
||||
id: SheetTableRemoveColCommand.id,
|
||||
params: {
|
||||
...params,
|
||||
tableName: tableInfo.name,
|
||||
removedColumnNames: columns.map((column) => column.displayName),
|
||||
},
|
||||
});
|
||||
redos.unshift(...(interceptorCommands.preRedos ?? []));
|
||||
redos.push(...interceptorCommands.redos);
|
||||
undos.unshift(...(interceptorCommands.preUndos ?? []));
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: removeColCount,
|
||||
columnsJson: columns,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push(...interceptorCommands.undos);
|
||||
|
||||
const worksheet = target.worksheet;
|
||||
const colContentIndex = worksheet.getCellMatrix().getDataRange().endColumn;
|
||||
const moveRangeMutations = getMoveRangeUndoRedoMutations(
|
||||
accessor,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: range.endColumn + 1,
|
||||
endColumn: colContentIndex,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: oldRange.startRow,
|
||||
endRow: oldRange.endRow,
|
||||
startColumn: range.startColumn,
|
||||
endColumn: colContentIndex - removeColCount,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (moveRangeMutations) {
|
||||
redos.push(...moveRangeMutations.redos);
|
||||
undos.push(...moveRangeMutations.undos);
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const res = sequenceExecute(redos, commandService);
|
||||
if (res.result) {
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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, vi } from 'vitest';
|
||||
import { TableManager } from '../../../models/table-manager';
|
||||
import { SheetTableService } from '../../../services/table.service';
|
||||
import { AddSheetTableMutation } from '../add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../delete-sheet-table.mutation';
|
||||
import { SetSheetTableMutation } from '../set-sheet-table.mutation';
|
||||
import { SetSheetTableFilterMutation } from '../set-table-filter.mutation';
|
||||
|
||||
function accessorForService(service: SheetTableService) {
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (token === SheetTableService) {
|
||||
return service;
|
||||
}
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
function accessorForTableManager(tableManager: TableManager) {
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (token === TableManager) {
|
||||
return tableManager;
|
||||
}
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('sheets-table mutations', () => {
|
||||
it('AddSheetTableMutation should delegate to SheetTableService.addTable', () => {
|
||||
const service = {
|
||||
addTable: vi.fn(),
|
||||
} as unknown as SheetTableService;
|
||||
|
||||
const result = AddSheetTableMutation.handler(accessorForService(service), {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
name: 'Table',
|
||||
header: ['A'],
|
||||
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
options: { showHeader: true },
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(service.addTable).toHaveBeenCalledWith(
|
||||
'u1',
|
||||
's1',
|
||||
'Table',
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
['A'],
|
||||
't1',
|
||||
{ showHeader: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('DeleteSheetTableMutation should delegate to SheetTableService.deleteTable', () => {
|
||||
const service = {
|
||||
deleteTable: vi.fn(),
|
||||
} as unknown as SheetTableService;
|
||||
|
||||
const result = DeleteSheetTableMutation.handler(accessorForService(service), {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(service.deleteTable).toHaveBeenCalledWith('u1', 's1', 't1');
|
||||
});
|
||||
|
||||
it('SetSheetTableMutation should return false without params and set config otherwise', () => {
|
||||
const tableManager = {
|
||||
setTableByConfig: vi.fn(),
|
||||
} as unknown as TableManager;
|
||||
|
||||
expect(SetSheetTableMutation.handler(accessorForTableManager(tableManager), undefined as any)).toBe(false);
|
||||
|
||||
const result = SetSheetTableMutation.handler(accessorForTableManager(tableManager), {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
config: { name: 'NewName' },
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(tableManager.setTableByConfig).toHaveBeenCalledWith('u1', 't1', { name: 'NewName' });
|
||||
});
|
||||
|
||||
it('SetSheetTableFilterMutation should delegate to TableManager.addFilter', () => {
|
||||
const tableManager = {
|
||||
addFilter: vi.fn(),
|
||||
} as unknown as TableManager;
|
||||
|
||||
const result = SetSheetTableFilterMutation.handler(accessorForTableManager(tableManager), {
|
||||
unitId: 'u1',
|
||||
tableId: 't1',
|
||||
column: 0,
|
||||
tableFilter: { filterType: 'manual', values: ['A'] } as any,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(tableManager.addFilter).toHaveBeenCalledWith('u1', 't1', 0, { filterType: 'manual', values: ['A'] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { ITableOptions, ITableRange } from '../../types/type';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { SheetTableService } from '../../services/table.service';
|
||||
|
||||
export interface IAddSheetTableParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
name: string;
|
||||
range: ITableRange;
|
||||
// TODO: Parameters here should be aggregated with Meta, Columns, Options, etc.
|
||||
options?: ITableOptions;
|
||||
tableId?: string;
|
||||
header?: string[];
|
||||
}
|
||||
|
||||
export const AddSheetTableMutation: IMutation<IAddSheetTableParams & { tableId: string }> = {
|
||||
id: 'sheet.mutation.add-table',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { tableId, unitId, subUnitId, name, range, header, options } = params;
|
||||
const sheetTableService = accessor.get(SheetTableService);
|
||||
sheetTableService.addTable(unitId, subUnitId, name, range, header, tableId, options);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { SheetTableService } from '../../services/table.service';
|
||||
|
||||
export interface IDeleteSheetTableParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
export const DeleteSheetTableMutation: IMutation<IDeleteSheetTableParams> = {
|
||||
id: 'sheet.mutation.delete-table',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { unitId, subUnitId, tableId } = params;
|
||||
const sheetTableService = accessor.get(SheetTableService);
|
||||
sheetTableService.deleteTable(unitId, subUnitId, tableId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { ITableSetConfig } from '../../types/type';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
|
||||
export interface ISetSheetTableMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
config: ITableSetConfig;
|
||||
}
|
||||
|
||||
export const SetSheetTableMutation: IMutation<ISetSheetTableMutationParams> = {
|
||||
id: 'sheet.mutation.set-sheet-table',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, tableId, config } = params;
|
||||
const tableManager = accessor.get(TableManager);
|
||||
tableManager.setTableByConfig(unitId, tableId, config);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { ITableFilterItem } from '../../types/type';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
|
||||
export interface ISetSheetTableParams {
|
||||
unitId: string;
|
||||
tableId: string;
|
||||
column: number;
|
||||
tableFilter: ITableFilterItem | undefined;
|
||||
}
|
||||
|
||||
export const SetSheetTableFilterMutation: IMutation<ISetSheetTableParams & { tableId: string }> = {
|
||||
id: 'sheet.mutation.set-table-filter',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { tableId, unitId, column, tableFilter } = params;
|
||||
const tableManager = accessor.get(TableManager);
|
||||
tableManager.addFilter(unitId, tableId, column, tableFilter);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
40
packages/sheets-table/src/config/config.ts
Normal file
40
packages/sheets-table/src/config/config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRangeThemeStyleJSON } from '@univerjs/sheets';
|
||||
import { BorderStyleTypes } from '@univerjs/core';
|
||||
|
||||
export interface ITableDefaultThemeStyle {
|
||||
name: string;
|
||||
style: Omit<IRangeThemeStyleJSON, 'name'>;
|
||||
}
|
||||
export const tableDefaultBorderStyle = {
|
||||
s: BorderStyleTypes.THIN,
|
||||
cl: {
|
||||
rgb: 'rgb(95, 101, 116)',
|
||||
},
|
||||
};
|
||||
|
||||
export const SHEETS_TABLE_PLUGIN_CONFIG_KEY = 'sheets-table.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_TABLE_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsTableConfig {
|
||||
userThemes?: ITableDefaultThemeStyle[];
|
||||
defaultThemeIndex?: number;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsTableConfig = {};
|
||||
21
packages/sheets-table/src/const.ts
Normal file
21
packages/sheets-table/src/const.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 const PLUGIN_NAME = 'SHEET_TABLE_PLUGIN';
|
||||
export const FEATURE_TABLE_ID = 'SHEET_TABLE';
|
||||
export const DEFAULT_COLUMN_NAME_PREFIX = 'Column';
|
||||
export const SHEET_TABLE_CUSTOM_THEME_PREFIX = 'table-custom';
|
||||
export const TABLE_FILTER_EMPTY_VALUE = '__UNIVER_TABLE_FILTER_EMPTY__';
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RemoveSuperTableMutation, SetSuperTableMutation } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetTableFormulaController } from '../sheet-table-formula.controller';
|
||||
|
||||
describe('SheetTableFormulaController', () => {
|
||||
it('should sync super table metadata on add/range/name/delete events', () => {
|
||||
const tableAdd$ = new Subject<any>();
|
||||
const tableRangeChanged$ = new Subject<any>();
|
||||
const tableDelete$ = new Subject<any>();
|
||||
const tableNameChanged$ = new Subject<any>();
|
||||
|
||||
const table = {
|
||||
getTableInfo: () => ({
|
||||
name: 'Orders',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
|
||||
columns: [
|
||||
{ displayName: 'id' },
|
||||
{ displayName: 'amount' },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
tableAdd$,
|
||||
tableRangeChanged$,
|
||||
tableDelete$,
|
||||
tableNameChanged$,
|
||||
getTableById: vi.fn(() => table),
|
||||
};
|
||||
|
||||
const executeCommand = vi.fn();
|
||||
const commandService = { executeCommand };
|
||||
|
||||
const controller = new SheetTableFormulaController(tableManager as any, commandService as any);
|
||||
|
||||
tableAdd$.next({ unitId: 'u1', tableId: 't1' });
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(SetSuperTableMutation.id, expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
tableName: 'Orders',
|
||||
reference: expect.objectContaining({
|
||||
sheetId: 's1',
|
||||
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
|
||||
}),
|
||||
}), { onlyLocal: true });
|
||||
|
||||
const titleMap = executeCommand.mock.calls[0][1].reference.titleMap as Map<string, number>;
|
||||
expect(titleMap.get('id')).toBe(0);
|
||||
expect(titleMap.get('amount')).toBe(1);
|
||||
|
||||
tableRangeChanged$.next({ unitId: 'u1', tableId: 't1' });
|
||||
expect(executeCommand).toHaveBeenCalledTimes(2);
|
||||
|
||||
tableNameChanged$.next({ unitId: 'u1', tableId: 't1', oldTableName: 'Orders_Old' });
|
||||
expect(executeCommand).toHaveBeenNthCalledWith(3, RemoveSuperTableMutation.id, {
|
||||
unitId: 'u1',
|
||||
tableName: 'Orders_Old',
|
||||
}, { onlyLocal: true });
|
||||
expect(executeCommand).toHaveBeenCalledTimes(4);
|
||||
|
||||
tableDelete$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableName: 'Orders',
|
||||
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
|
||||
});
|
||||
expect(executeCommand).toHaveBeenLastCalledWith(RemoveSuperTableMutation.id, {
|
||||
unitId: 'u1',
|
||||
tableName: 'Orders',
|
||||
reference: {
|
||||
sheetId: 's1',
|
||||
range: { startRow: 0, endRow: 5, startColumn: 0, endColumn: 2 },
|
||||
},
|
||||
}, { onlyLocal: true });
|
||||
expect(executeCommand.mock.calls.every((call) => call[2]?.onlyLocal === true)).toBe(true);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,850 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, ICellData, IWorkbookData, Nullable, UnitModel, Workbook } from '@univerjs/core';
|
||||
import type { BaseAstNode, BaseFunction, IUnitData } from '@univerjs/engine-formula';
|
||||
import type { IRender } from '@univerjs/engine-render';
|
||||
import {
|
||||
CellValueType,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
ObjectMatrix,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
set,
|
||||
ThemeService,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AstRootNodeFactory,
|
||||
AstTreeBuilder,
|
||||
CalculateFormulaService,
|
||||
DefinedNamesService,
|
||||
FormulaCurrentConfigService,
|
||||
FormulaDataModel,
|
||||
FormulaDependencyGenerator,
|
||||
FormulaRuntimeService,
|
||||
FormulaUnitReferenceResolver,
|
||||
functionMath,
|
||||
FunctionNodeFactory,
|
||||
FunctionService,
|
||||
generateExecuteAstNodeData,
|
||||
getObjectValue,
|
||||
HyperlinkEngineFormulaService,
|
||||
ICalculateFormulaService,
|
||||
IDefinedNamesService,
|
||||
IFormulaCurrentConfigService,
|
||||
IFormulaDependencyGenerator,
|
||||
IFormulaExternalReferenceDataLoader,
|
||||
IFormulaRuntimeService,
|
||||
IFormulaUnitReferenceResolver,
|
||||
IFunctionService,
|
||||
IHyperlinkEngineFormulaService,
|
||||
Interpreter,
|
||||
IOtherFormulaManagerService,
|
||||
ISheetRowFilteredService,
|
||||
ISuperTableService,
|
||||
LambdaNodeFactory,
|
||||
LambdaParameterNodeFactory,
|
||||
Lexer,
|
||||
LexerNode,
|
||||
LexerTreeBuilder,
|
||||
NoopFormulaExternalReferenceDataLoader,
|
||||
OperatorNodeFactory,
|
||||
OtherFormulaManagerService,
|
||||
PrefixNodeFactory,
|
||||
ReferenceNodeFactory,
|
||||
RemoveSuperTableMutation,
|
||||
SetArrayFormulaDataMutation,
|
||||
SetFormulaDataMutation,
|
||||
SetSuperTableMutation,
|
||||
SheetRowFilteredService,
|
||||
SuffixNodeFactory,
|
||||
SuperTableService,
|
||||
UnionNodeFactory,
|
||||
ValueNodeFactory,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { Engine, IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { InsertColMutation, InsertRowMutation, MoveRangeCommand, MoveRangeMutation, RangeProtectionRuleModel, RefRangeService, RemoveColByRangeCommand, RemoveColCommand, RemoveColMutation, RemoveRowByRangeCommand, RemoveRowCommand, RemoveRowMutation, SetRangeValuesCommand, SetRangeValuesMutation, SheetInterceptorService, SheetSkeletonService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets';
|
||||
import { UpdateFormulaController } from '@univerjs/sheets-formula';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets/locale/zh-CN';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DeleteSheetTableCommand } from '../../commands/commands/delete-sheet-table.command';
|
||||
import { SetSheetTableCommand } from '../../commands/commands/set-sheet-table.command';
|
||||
import { SheetTableInsertColumnAtCommand, SheetTableRemoveColumnAtCommand } from '../../commands/commands/sheet-table-row-col.command';
|
||||
import { AddSheetTableMutation } from '../../commands/mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../../commands/mutations/delete-sheet-table.mutation';
|
||||
import { SetSheetTableMutation } from '../../commands/mutations/set-sheet-table.mutation';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { SheetTableService } from '../../services/table.service';
|
||||
import { SheetTableFormulaController } from '../sheet-table-formula.controller';
|
||||
import { SheetTableRefRangeController } from '../sheet-table-ref-range.controller';
|
||||
|
||||
interface ITestBed {
|
||||
univer: Univer;
|
||||
sheet: UnitModel<IWorkbookData>;
|
||||
injector: Injector;
|
||||
}
|
||||
|
||||
class RenderManagerServiceTestBed extends RenderManagerService {
|
||||
override createRender(unitId: string): IRender {
|
||||
const renderer = this._createRender(unitId, new Engine('', { elementHeight: 100, elementWidth: 100 }));
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
function createFacadeTestBed(workbookData: IWorkbookData, dependencies: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerServiceTestBed }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([RefRangeService]);
|
||||
injector.add([WorksheetPermissionService]);
|
||||
injector.add([WorkbookPermissionService]);
|
||||
injector.add([WorksheetProtectionPointModel]);
|
||||
injector.add([RangeProtectionRuleModel]);
|
||||
injector.add([WorksheetProtectionRuleModel]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
|
||||
dependencies.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
this._injector.get(WorkbookPermissionService);
|
||||
this._injector.get(WorksheetPermissionService);
|
||||
}
|
||||
}
|
||||
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
const themeService = injector.get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
injector.get(IUniverInstanceService).focusUnit('test');
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
sheet,
|
||||
injector,
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'test',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'product', t: CellValueType.STRING },
|
||||
1: { v: 'amount', t: CellValueType.STRING },
|
||||
2: { v: 'tax', t: CellValueType.STRING },
|
||||
},
|
||||
1: {
|
||||
0: { v: 'A', t: CellValueType.STRING },
|
||||
1: { v: 10, t: CellValueType.NUMBER },
|
||||
2: { v: 1, t: CellValueType.NUMBER },
|
||||
},
|
||||
2: {
|
||||
0: { v: 'B', t: CellValueType.STRING },
|
||||
1: { v: 20, t: CellValueType.NUMBER },
|
||||
2: { v: 2, t: CellValueType.NUMBER },
|
||||
},
|
||||
3: {
|
||||
0: { v: 'C', t: CellValueType.STRING },
|
||||
1: { v: 30, t: CellValueType.NUMBER },
|
||||
2: { v: 3, t: CellValueType.NUMBER },
|
||||
},
|
||||
5: {
|
||||
4: { f: '=SUM(Orders[amount])' },
|
||||
5: { f: '="Orders[amount]"' },
|
||||
6: { f: '=Table[1]' },
|
||||
7: { f: '=Table[2]' },
|
||||
8: { f: '="Table[1]"' },
|
||||
},
|
||||
6: {
|
||||
4: { f: '=SUM(Orders[amount],Orders[amount])' },
|
||||
},
|
||||
7: {
|
||||
4: { f: '=SUM(Orders[amount])' },
|
||||
},
|
||||
},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
2: {
|
||||
0: { f: '=SUM(Orders)' },
|
||||
},
|
||||
3: {
|
||||
0: { f: '=SUM(Orders[amount])' },
|
||||
},
|
||||
4: {
|
||||
0: { f: '=SUM(Orders[amount],Orders[tax])' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createControllerTestBed() {
|
||||
const dependencies: Dependency[] = [
|
||||
[ICalculateFormulaService, { useClass: CalculateFormulaService }],
|
||||
[Lexer],
|
||||
[IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }],
|
||||
[IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }],
|
||||
[IFormulaExternalReferenceDataLoader, { useClass: NoopFormulaExternalReferenceDataLoader }],
|
||||
[IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }],
|
||||
[IFormulaRuntimeService, { useClass: FormulaRuntimeService }],
|
||||
[IOtherFormulaManagerService, { useClass: OtherFormulaManagerService }],
|
||||
[ISheetRowFilteredService, { useClass: SheetRowFilteredService }],
|
||||
[IFormulaDependencyGenerator, { useClass: FormulaDependencyGenerator }],
|
||||
[Interpreter],
|
||||
[AstTreeBuilder],
|
||||
[AstRootNodeFactory],
|
||||
[FunctionNodeFactory],
|
||||
[LambdaNodeFactory],
|
||||
[LambdaParameterNodeFactory],
|
||||
[OperatorNodeFactory],
|
||||
[PrefixNodeFactory],
|
||||
[ReferenceNodeFactory],
|
||||
[SuffixNodeFactory],
|
||||
[UnionNodeFactory],
|
||||
[ValueNodeFactory],
|
||||
[ISuperTableService, { useClass: SuperTableService }],
|
||||
[TableManager],
|
||||
[SheetTableService],
|
||||
[SheetTableFormulaController],
|
||||
[SheetTableRefRangeController],
|
||||
[UpdateFormulaController],
|
||||
];
|
||||
|
||||
return createFacadeTestBed(createWorkbookData(), dependencies);
|
||||
}
|
||||
|
||||
describe('Sheet table formula integration', () => {
|
||||
let testBed: ReturnType<typeof createControllerTestBed>;
|
||||
let commandService: ICommandService;
|
||||
let formulaDataModel: FormulaDataModel;
|
||||
let calculate: (formula: string, row?: number, column?: number, sheetId?: string) => string | number | boolean | null | (string | number | boolean | null)[][];
|
||||
|
||||
function getFormula(unitId: string, sheetId: string, row: number, column: number) {
|
||||
return formulaDataModel.getFormulaData()[unitId]?.[sheetId]?.[row]?.[column]?.f;
|
||||
}
|
||||
|
||||
function getCellFormula(sheetId: string, row: number, column: number) {
|
||||
return testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId(sheetId)
|
||||
?.getRange(row, column, row, column)
|
||||
.getValue()
|
||||
?.f;
|
||||
}
|
||||
|
||||
function getAllUnitData() {
|
||||
const workbook = testBed.injector.get(IUniverInstanceService).getUnit<Workbook>('test')!;
|
||||
const sheetData: IUnitData[string] = {};
|
||||
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
const sheetConfig = sheet.getConfig();
|
||||
sheetData[sheet.getSheetId()] = {
|
||||
cellData: new ObjectMatrix(sheetConfig.cellData),
|
||||
rowCount: sheetConfig.rowCount,
|
||||
columnCount: sheetConfig.columnCount,
|
||||
rowData: sheetConfig.rowData ?? {},
|
||||
columnData: sheetConfig.columnData ?? {},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
[workbook.getUnitId()]: sheetData,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCellFormula(unitId: string, sheetId: string, row: number, column: number) {
|
||||
const formula = getFormula(unitId, sheetId, row, column);
|
||||
expect(formula).toBeDefined();
|
||||
|
||||
return calculate(formula!, row, column, sheetId);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testBed = createControllerTestBed();
|
||||
commandService = testBed.injector.get(ICommandService);
|
||||
formulaDataModel = testBed.injector.get(FormulaDataModel);
|
||||
|
||||
commandService.registerCommand(RemoveRowCommand);
|
||||
commandService.registerCommand(RemoveColCommand);
|
||||
commandService.registerCommand(MoveRangeCommand);
|
||||
commandService.registerCommand(MoveRangeMutation);
|
||||
commandService.registerCommand(RemoveRowByRangeCommand);
|
||||
commandService.registerCommand(RemoveColByRangeCommand);
|
||||
commandService.registerCommand(RemoveRowMutation);
|
||||
commandService.registerCommand(RemoveColMutation);
|
||||
commandService.registerCommand(InsertRowMutation);
|
||||
commandService.registerCommand(InsertColMutation);
|
||||
commandService.registerCommand(AddSheetTableMutation);
|
||||
commandService.registerCommand(DeleteSheetTableCommand);
|
||||
commandService.registerCommand(DeleteSheetTableMutation);
|
||||
commandService.registerCommand(SetSheetTableCommand);
|
||||
commandService.registerCommand(SetSheetTableMutation);
|
||||
commandService.registerCommand(SheetTableInsertColumnAtCommand);
|
||||
commandService.registerCommand(SheetTableRemoveColumnAtCommand);
|
||||
commandService.registerCommand(SetSuperTableMutation);
|
||||
commandService.registerCommand(RemoveSuperTableMutation);
|
||||
commandService.registerCommand(SetRangeValuesCommand);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetFormulaDataMutation);
|
||||
commandService.registerCommand(SetArrayFormulaDataMutation);
|
||||
|
||||
testBed.injector.get(SheetTableFormulaController);
|
||||
testBed.injector.get(SheetTableRefRangeController);
|
||||
testBed.injector.get(UpdateFormulaController);
|
||||
|
||||
commandService.syncExecuteCommand(AddSheetTableMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
name: 'Orders',
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
endColumn: 2,
|
||||
},
|
||||
header: ['product', 'amount', 'tax'],
|
||||
options: {
|
||||
columns: [
|
||||
{ id: 'product-column', displayName: 'product' },
|
||||
{ id: 'amount-column', displayName: 'amount' },
|
||||
{ id: 'tax-column', displayName: 'tax' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const functionService = testBed.injector.get(IFunctionService);
|
||||
const mathFunctions = functionMath as Array<[new (name: string) => BaseFunction, string]>;
|
||||
functionService.registerExecutors(...mathFunctions.map(([FunctionCtor, name]) => new FunctionCtor(name)));
|
||||
|
||||
calculate = (formula: string, row = 0, column = 0, sheetId = 'sheet1') => {
|
||||
const workbook = testBed.injector.get(IUniverInstanceService).getUnit<Workbook>('test')!;
|
||||
const worksheet = workbook.getSheetBySheetId(sheetId)!;
|
||||
const formulaCurrentConfigService = testBed.injector.get(IFormulaCurrentConfigService);
|
||||
const formulaRuntimeService = testBed.injector.get(IFormulaRuntimeService);
|
||||
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: formulaDataModel.getFormulaData(),
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtySuperTableMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: getAllUnitData(),
|
||||
});
|
||||
formulaRuntimeService.setCurrent(row, column, worksheet.getRowCount(), worksheet.getColumnCount(), sheetId, workbook.getUnitId());
|
||||
|
||||
const lexerNode = testBed.injector.get(Lexer).treeBuilder(formula);
|
||||
if (!(lexerNode instanceof LexerNode)) {
|
||||
throw new TypeError(`Failed to parse formula: ${formula}`);
|
||||
}
|
||||
const astNode = testBed.injector.get(AstTreeBuilder).parse(lexerNode);
|
||||
const result = testBed.injector.get(Interpreter).execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
return getObjectValue(result);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
testBed?.univer.dispose();
|
||||
});
|
||||
|
||||
it('should sync table formulas when a table is renamed, undone, and redone', async () => {
|
||||
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
tableId: 'orders-table',
|
||||
name: 'Sales',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
});
|
||||
|
||||
it('should sync wrapped repeated table references when a table is renamed, removed, and has a referenced column removed', async () => {
|
||||
expect(calculateCellFormula('test', 'sheet1', 6, 4)).toBe(120);
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
tableId: 'orders-table',
|
||||
name: 'Sales',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 6, 4)).toBe(120);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Sales[amount],Sales[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
index: 1,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(Orders[amount],Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
expect(getCellFormula('sheet1', 6, 4)).toBe('=SUM(#REF!,#REF!)');
|
||||
});
|
||||
|
||||
it('should sync multiple table formulas across sheets when a table changes, then undo and redo', async () => {
|
||||
expect(calculateCellFormula('test', 'sheet1', 7, 4)).toBe(60);
|
||||
expect(calculateCellFormula('test', 'sheet2', 3, 0)).toBe(60);
|
||||
expect(calculateCellFormula('test', 'sheet2', 4, 0)).toBe(66);
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
tableId: 'orders-table',
|
||||
name: 'Sales',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Sales[amount])');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Sales[amount],Sales[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Sales[amount])');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Sales[amount])');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Sales[amount],Sales[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
index: 1,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,Orders[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,Orders[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
|
||||
|
||||
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,#REF!)');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(Orders[amount])');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(Orders[amount],Orders[tax])');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 7, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 3, 0)).toBe('=SUM(#REF!)');
|
||||
expect(getFormula('test', 'sheet2', 4, 0)).toBe('=SUM(#REF!,#REF!)');
|
||||
});
|
||||
|
||||
it('should recalculate table formulas when table columns shrink, then undo and redo the column insertion', async () => {
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
|
||||
expect(await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(60);
|
||||
});
|
||||
|
||||
it('should recalculate table formulas when table columns are inserted, undone, and redone', async () => {
|
||||
const tableManager = testBed.injector.get(TableManager);
|
||||
const getTableRange = () => tableManager.getTableById('test', 'orders-table')?.getRange();
|
||||
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 2 });
|
||||
|
||||
expect(await commandService.executeCommand(SheetTableInsertColumnAtCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
index: 2,
|
||||
count: 1,
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 3 });
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 2 });
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getCellFormula('sheet2', 2, 0)).toBe('=SUM(Orders)');
|
||||
expect(getTableRange()).toMatchObject({ startColumn: 0, endColumn: 3 });
|
||||
expect(calculateCellFormula('test', 'sheet2', 2, 0)).toBe(66);
|
||||
});
|
||||
|
||||
it('should recalculate table column formulas when worksheet rows are removed, undone, and redone', async () => {
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RemoveRowCommand.id, {
|
||||
range: {
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
},
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 4, 4)).toBe(40);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 4, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 4, 4)).toBe(40);
|
||||
});
|
||||
|
||||
it('should sync table column formulas when worksheet columns are removed, undone, and redone', async () => {
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id, {
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 19,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 5, 3)).toBe('=SUM(#REF!)');
|
||||
expect(getCellFormula('sheet1', 5, 3)).toBe('=SUM(#REF!)');
|
||||
expect(calculate('=SUM(#REF!)', 5, 3)).toBe('#REF!');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 3)).toBe('=SUM(#REF!)');
|
||||
expect(getCellFormula('sheet1', 5, 3)).toBe('=SUM(#REF!)');
|
||||
expect(calculate('=SUM(#REF!)', 5, 3)).toBe('#REF!');
|
||||
});
|
||||
|
||||
it('should sync table formulas when a table is deleted, undone, and redone', async () => {
|
||||
expect(await commandService.executeCommand(DeleteSheetTableCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'orders-table',
|
||||
})).toBe(true);
|
||||
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(#REF!)');
|
||||
expect(calculate('=SUM(#REF!)', 5, 4)).toBe('#REF!');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(Orders[amount])');
|
||||
expect(calculateCellFormula('test', 'sheet1', 5, 4)).toBe(60);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(getFormula('test', 'sheet1', 5, 4)).toBe('=SUM(#REF!)');
|
||||
expect(getCellFormula('sheet1', 5, 4)).toBe('=SUM(#REF!)');
|
||||
expect(calculate('=SUM(#REF!)', 5, 4)).toBe('#REF!');
|
||||
});
|
||||
|
||||
it('should rewrite formulas when a referenced table is renamed', async () => {
|
||||
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
|
||||
const renameTableResult = sheetInterceptorService.onCommandExecute({
|
||||
id: 'sheet.command.set-table-config',
|
||||
params: {
|
||||
unitId: 'test',
|
||||
tableId: 'table-1',
|
||||
name: 'Sales',
|
||||
oldTableName: 'Orders',
|
||||
},
|
||||
});
|
||||
|
||||
for (const mutation of renameTableResult.redos) {
|
||||
await commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
|
||||
const values = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 4, 5, 4)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(values).toEqual([[{ f: '=SUM(Sales[amount])' }]]);
|
||||
|
||||
const stringLiteralValues = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 5, 5, 5)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(stringLiteralValues).toEqual([[{ f: '="Orders[amount]"' }]]);
|
||||
});
|
||||
|
||||
it('should rewrite formulas when a referenced table is removed', async () => {
|
||||
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
|
||||
const removeTableResult = sheetInterceptorService.onCommandExecute({
|
||||
id: 'sheet.command.delete-table',
|
||||
params: {
|
||||
unitId: 'test',
|
||||
tableId: 'table-1',
|
||||
tableName: 'Orders',
|
||||
},
|
||||
});
|
||||
|
||||
for (const mutation of removeTableResult.redos) {
|
||||
await commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
|
||||
const values = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 4, 5, 4)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(values).toEqual([[{ f: '=SUM(#REF!)' }]]);
|
||||
|
||||
const stringLiteralValues = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 5, 5, 5)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(stringLiteralValues).toEqual([[{ f: '="Orders[amount]"' }]]);
|
||||
});
|
||||
|
||||
it('should rewrite numeric table column references when the referenced table column is removed', async () => {
|
||||
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
|
||||
const removeTableColumnResult = sheetInterceptorService.onCommandExecute({
|
||||
id: 'sheet.command.table-remove-column-at',
|
||||
params: {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'table-1',
|
||||
tableName: 'Table',
|
||||
removedColumnNames: ['1'],
|
||||
},
|
||||
});
|
||||
|
||||
for (const mutation of removeTableColumnResult.redos) {
|
||||
await commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
|
||||
const values = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 6, 5, 8)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(values).toEqual([[
|
||||
{ f: '=#REF!' },
|
||||
{ f: '=Table[2]' },
|
||||
{ f: '="Table[1]"' },
|
||||
]]);
|
||||
});
|
||||
|
||||
it('should move formula cells when worksheet column removal deletes a referenced table column', async () => {
|
||||
const sheetInterceptorService = testBed.injector.get(SheetInterceptorService);
|
||||
const removeTableColumnResult = sheetInterceptorService.onCommandExecute({
|
||||
id: 'sheet.command.table-remove-col',
|
||||
params: {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
tableId: 'table-1',
|
||||
tableName: 'Table',
|
||||
range: { startRow: 0, endRow: 19, startColumn: 0, endColumn: 0 },
|
||||
removedColumnNames: ['1'],
|
||||
},
|
||||
});
|
||||
|
||||
for (const mutation of removeTableColumnResult.redos) {
|
||||
await commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
|
||||
const values = testBed.injector
|
||||
.get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test')
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(5, 5, 5, 8)
|
||||
.getValues() as Array<Array<Nullable<ICellData>>>;
|
||||
|
||||
expect(values).toEqual([[{ f: '=#REF!' }, { f: '=Table[2]' }, { f: '="Table[1]"' }, null]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FEATURE_TABLE_ID } from '../../const';
|
||||
import { SheetTableRangeController } from '../sheet-table-range.controller';
|
||||
|
||||
describe('SheetTableRangeController', () => {
|
||||
it('should sync exclusive ranges on table add/range change/delete', () => {
|
||||
const tableRangeChanged$ = new Subject<any>();
|
||||
const tableAdd$ = new Subject<any>();
|
||||
const tableDelete$ = new Subject<any>();
|
||||
|
||||
const tableManager = {
|
||||
tableRangeChanged$,
|
||||
tableAdd$,
|
||||
tableDelete$,
|
||||
};
|
||||
|
||||
const exclusiveRangeService = {
|
||||
clearExclusiveRangesByGroupId: vi.fn(),
|
||||
addExclusiveRange: vi.fn(),
|
||||
};
|
||||
|
||||
const controller = new SheetTableRangeController(tableManager as any, exclusiveRangeService as any);
|
||||
|
||||
tableAdd$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 },
|
||||
});
|
||||
|
||||
expect(exclusiveRangeService.addExclusiveRange).toHaveBeenCalledWith('u1', 's1', FEATURE_TABLE_ID, [{
|
||||
range: { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 },
|
||||
groupId: 't1',
|
||||
}]);
|
||||
|
||||
tableRangeChanged$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
range: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 },
|
||||
});
|
||||
|
||||
expect(exclusiveRangeService.clearExclusiveRangesByGroupId).toHaveBeenCalledWith('u1', 's1', FEATURE_TABLE_ID, 't1');
|
||||
expect(exclusiveRangeService.addExclusiveRange).toHaveBeenLastCalledWith('u1', 's1', FEATURE_TABLE_ID, [{
|
||||
range: { startRow: 2, endRow: 4, startColumn: 1, endColumn: 3 },
|
||||
groupId: 't1',
|
||||
}]);
|
||||
|
||||
tableDelete$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
});
|
||||
|
||||
expect(exclusiveRangeService.clearExclusiveRangesByGroupId).toHaveBeenLastCalledWith('u1', 's1', FEATURE_TABLE_ID, 't1');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InsertRowMutation, RemoveColCommand } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetTableRefRangeController } from '../sheet-table-ref-range.controller';
|
||||
|
||||
describe('SheetTableRefRangeController', () => {
|
||||
it('should return empty mutations for unsupported commands and update table range after row insertion', () => {
|
||||
let interceptCommandConfig: any;
|
||||
let commandExecutedListener: any;
|
||||
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 5, endRow: 10, startColumn: 1, endColumn: 3 }),
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
getTablesBySubunitId: vi.fn(() => [table]),
|
||||
updateTableRange: vi.fn(),
|
||||
};
|
||||
|
||||
const controller = new SheetTableRefRangeController(
|
||||
{
|
||||
onCommandExecuted: vi.fn((listener: any) => {
|
||||
commandExecutedListener = listener;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{
|
||||
interceptCommand: vi.fn((config: any) => {
|
||||
interceptCommandConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any,
|
||||
tableManager as any,
|
||||
{ t: () => 'Column' } as any
|
||||
);
|
||||
|
||||
expect(interceptCommandConfig.getMutations({ id: 'unknown.command', params: {} })).toEqual({ redos: [], undos: [] });
|
||||
|
||||
commandExecutedListener({
|
||||
id: InsertRowMutation.id,
|
||||
params: {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 2, startColumn: 0, endColumn: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(tableManager.updateTableRange).toHaveBeenCalledWith('u1', 't1', {
|
||||
newRange: {
|
||||
startRow: 7,
|
||||
endRow: 12,
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should include table formula reference mutations when worksheet column removal deletes table columns', () => {
|
||||
let interceptCommandConfig: any;
|
||||
const tableColumn = {
|
||||
toJSON: () => ({ id: 'c1', displayName: '1' }),
|
||||
};
|
||||
const table = {
|
||||
getId: () => 't1',
|
||||
getRange: () => ({ startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 }),
|
||||
getTableInfo: () => ({ name: 'Table' }),
|
||||
getTableColumnByIndex: vi.fn(() => tableColumn),
|
||||
toJSON: () => ({
|
||||
id: 't1',
|
||||
name: 'Table',
|
||||
range: { startRow: 0, endRow: 5, startColumn: 1, endColumn: 3 },
|
||||
options: { showHeader: true },
|
||||
filters: {},
|
||||
columns: [{ id: 'c1', displayName: '1' }, { id: 'c2', displayName: '2' }, { id: 'c3', displayName: '3' }],
|
||||
}),
|
||||
};
|
||||
const workbook = {
|
||||
getUnitId: () => 'u1',
|
||||
getActiveSheet: () => ({ getSheetId: () => 's1' }),
|
||||
};
|
||||
const onCommandExecute = vi.fn(() => ({
|
||||
preRedos: [{ id: 'formula.pre-redo', params: {} }],
|
||||
redos: [{ id: 'formula.redo', params: {} }],
|
||||
preUndos: [{ id: 'formula.pre-undo', params: {} }],
|
||||
undos: [{ id: 'formula.undo', params: {} }],
|
||||
}));
|
||||
|
||||
const controller = new SheetTableRefRangeController(
|
||||
{ onCommandExecuted: vi.fn(() => ({ dispose: vi.fn() })) } as any,
|
||||
{} as any,
|
||||
{ getCurrentUnitOfType: () => workbook } as any,
|
||||
{} as any,
|
||||
{
|
||||
interceptCommand: vi.fn((config: any) => {
|
||||
interceptCommandConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
onCommandExecute,
|
||||
} as any,
|
||||
{
|
||||
getTablesBySubunitId: vi.fn(() => [table]),
|
||||
} as any,
|
||||
{ t: () => 'Column' } as any
|
||||
);
|
||||
|
||||
const result = interceptCommandConfig.getMutations({
|
||||
id: RemoveColCommand.id,
|
||||
params: {
|
||||
range: { startRow: 0, endRow: 99, startColumn: 1, endColumn: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(onCommandExecute).toHaveBeenCalledWith({
|
||||
id: 'sheet.command.table-remove-col',
|
||||
params: {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
tableName: 'Table',
|
||||
range: { startRow: 0, endRow: 99, startColumn: 1, endColumn: 1 },
|
||||
removedColumnNames: ['1'],
|
||||
},
|
||||
});
|
||||
expect(result.preRedos.map((mutation: any) => mutation.id)).toEqual([]);
|
||||
expect(result.redos.map((mutation: any) => mutation.id)).toEqual([
|
||||
'formula.pre-redo',
|
||||
'formula.redo',
|
||||
'sheet.mutation.set-sheet-table',
|
||||
]);
|
||||
expect(result.preUndos.map((mutation: any) => mutation.id)).toEqual([
|
||||
'sheet.mutation.delete-table',
|
||||
]);
|
||||
expect(result.undos.map((mutation: any) => mutation.id)).toEqual([
|
||||
'sheet.mutation.add-table',
|
||||
'formula.pre-undo',
|
||||
'formula.undo',
|
||||
]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsTableThemeController } from '../sheet-table-theme.controller';
|
||||
|
||||
describe('SheetsTableThemeController', () => {
|
||||
it('should initialize themes and react to table theme lifecycle events', () => {
|
||||
const tableAdd$ = new Subject<any>();
|
||||
const tableRangeChanged$ = new Subject<any>();
|
||||
const tableThemeChanged$ = new Subject<any>();
|
||||
const tableDelete$ = new Subject<any>();
|
||||
|
||||
const table = {
|
||||
getTableStyleId: vi.fn(() => undefined),
|
||||
setTableStyleId: vi.fn(),
|
||||
getRange: vi.fn(() => ({ startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 })),
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
tableAdd$,
|
||||
tableRangeChanged$,
|
||||
tableThemeChanged$,
|
||||
tableDelete$,
|
||||
getTable: vi.fn(() => table),
|
||||
};
|
||||
|
||||
const sheetRangeThemeService = {
|
||||
registerRangeThemeStyle: vi.fn(),
|
||||
removeRangeThemeRule: vi.fn(),
|
||||
};
|
||||
const sheetRangeThemeModel = {
|
||||
registerDefaultRangeTheme: vi.fn(),
|
||||
};
|
||||
const configService = {
|
||||
getConfig: vi.fn(() => ({
|
||||
defaultThemeIndex: 1,
|
||||
userThemes: [{ name: 'user-theme', style: {} }],
|
||||
})),
|
||||
};
|
||||
|
||||
const controller = new SheetsTableThemeController(
|
||||
tableManager as any,
|
||||
sheetRangeThemeService as any,
|
||||
sheetRangeThemeModel as any,
|
||||
configService as any
|
||||
);
|
||||
|
||||
expect(sheetRangeThemeModel.registerDefaultRangeTheme).toHaveBeenCalledTimes(7);
|
||||
|
||||
tableAdd$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
tableStyleId: '',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
expect(table.setTableStyleId).toHaveBeenCalledWith('table-default-0');
|
||||
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-0', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
tableRangeChanged$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
oldRange: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
range: { startRow: 1, endRow: 5, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-0', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 5, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
tableThemeChanged$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
oldTheme: 'table-default-0',
|
||||
theme: 'table-default-1',
|
||||
});
|
||||
|
||||
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
expect(sheetRangeThemeService.registerRangeThemeStyle).toHaveBeenCalledWith('table-default-1', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
tableDelete$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
expect(sheetRangeThemeService.removeRangeThemeRule).toHaveBeenCalledWith('table-default-0', {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 },
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsTableController } from '../sheets-table.controller';
|
||||
|
||||
describe('SheetsTableController', () => {
|
||||
it('should resolve containing table by range and inject header cell content', () => {
|
||||
const tableAdd$ = new Subject<any>();
|
||||
const tableRangeChanged$ = new Subject<any>();
|
||||
const tableDelete$ = new Subject<any>();
|
||||
|
||||
const tableRange = { startRow: 2, endRow: 6, startColumn: 1, endColumn: 4 };
|
||||
const table = {
|
||||
getRange: () => tableRange,
|
||||
getColumnNameByIndex: (index: number) => ['Name', 'Qty', 'Price', 'Note'][index],
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
tableAdd$,
|
||||
tableRangeChanged$,
|
||||
tableDelete$,
|
||||
getTable: vi.fn((_unitId: string, tableId: string) => (tableId === 't1' ? table : undefined)),
|
||||
getTablesBySubunitId: vi.fn(() => []),
|
||||
toJSON: vi.fn(() => ({})),
|
||||
fromJSON: vi.fn(),
|
||||
deleteUnitId: vi.fn(),
|
||||
};
|
||||
|
||||
let cellContentHandler: any;
|
||||
const sheetInterceptorService = {
|
||||
intercept: vi.fn((_point: any, config: any) => {
|
||||
cellContentHandler = config.handler;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
interceptCommand: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
};
|
||||
|
||||
const resourceManagerService = {
|
||||
registerPluginResource: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
};
|
||||
|
||||
const controller = new SheetsTableController(
|
||||
{
|
||||
getCurrentUnitOfType: vi.fn(() => null),
|
||||
} as any,
|
||||
sheetInterceptorService as any,
|
||||
tableManager as any,
|
||||
resourceManagerService as any
|
||||
);
|
||||
|
||||
tableAdd$.next({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
tableId: 't1',
|
||||
range: tableRange,
|
||||
});
|
||||
|
||||
expect(
|
||||
controller.getContainerTableWithRange('u1', 's1', { startRow: 3, endRow: 4, startColumn: 2, endColumn: 3 })
|
||||
).toBe(table);
|
||||
|
||||
expect(
|
||||
controller.getContainerTableWithRange('u1', 's1', { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 })
|
||||
).toBeUndefined();
|
||||
|
||||
const next = vi.fn((cell) => cell);
|
||||
const result = cellContentHandler(undefined, {
|
||||
row: 2,
|
||||
col: 2,
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
rawData: { s: 1 },
|
||||
}, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith({ s: 1, v: 'Qty' });
|
||||
expect(result).toEqual({ s: 1, v: 'Qty' });
|
||||
|
||||
const nextNonHeader = vi.fn((cell) => cell);
|
||||
cellContentHandler(undefined, {
|
||||
row: 3,
|
||||
col: 2,
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
rawData: { s: 1 },
|
||||
}, nextNonHeader);
|
||||
|
||||
expect(nextNonHeader).toHaveBeenCalledWith(undefined);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DateSystem } from '@univerjs/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TableFilterController } from '../table-filter.controller';
|
||||
|
||||
describe('TableFilterController', () => {
|
||||
it('should merge filtered rows from table filter events and initialize intercept behavior', () => {
|
||||
const tableFilterChanged$ = new Subject<any>();
|
||||
const tableInitStatus$ = new BehaviorSubject(false);
|
||||
|
||||
const mainFilter = {
|
||||
doFilter: vi.fn(),
|
||||
getFilterOutRows: () => new Set([4]),
|
||||
};
|
||||
const mainTable = {
|
||||
getTableFilters: () => mainFilter,
|
||||
getTableFilterRange: () => ({ startRow: 1, endRow: 10, startColumn: 1, endColumn: 2 }),
|
||||
};
|
||||
|
||||
const extraTable = {
|
||||
getTableFilters: () => ({
|
||||
getFilterOutRows: () => new Set([8]),
|
||||
}),
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
tableFilterChanged$,
|
||||
tableInitStatus$,
|
||||
getTable: vi.fn(() => mainTable),
|
||||
getTablesBySubunitId: vi.fn(() => [mainTable, extraTable]),
|
||||
};
|
||||
|
||||
let rowFilteredHandler: any;
|
||||
const sheetInterceptorService = {
|
||||
intercept: vi.fn((_point: any, config: any) => {
|
||||
rowFilteredHandler = config.handler;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
};
|
||||
|
||||
const activeSheet$ = new Subject<any>();
|
||||
const workbook = {
|
||||
getUnitId: () => 'u1',
|
||||
getActiveSheet: () => ({ getSheetId: () => 's1' }),
|
||||
activeSheet$,
|
||||
getSheetBySheetId: vi.fn(() => ({ id: 'sheet1' })),
|
||||
getDateSystem: () => DateSystem.Date1904,
|
||||
};
|
||||
|
||||
const workbookType$ = new Subject<any>();
|
||||
const univerInstanceService = {
|
||||
getCurrentTypeOfUnit$: vi.fn(() => workbookType$),
|
||||
getCurrentUnitOfType: vi.fn(() => workbook),
|
||||
getUnit: vi.fn(() => workbook),
|
||||
};
|
||||
|
||||
const zebraCrossingCacheController = {
|
||||
updateZebraCrossingCache: vi.fn(),
|
||||
};
|
||||
|
||||
const controller = new TableFilterController(
|
||||
tableManager as any,
|
||||
sheetInterceptorService as any,
|
||||
univerInstanceService as any,
|
||||
zebraCrossingCacheController as any
|
||||
);
|
||||
|
||||
tableFilterChanged$.next({ unitId: 'u1', subUnitId: 's1', tableId: 't1' });
|
||||
|
||||
expect(mainFilter.doFilter).toHaveBeenCalledWith(
|
||||
{ id: 'sheet1' },
|
||||
{ startRow: 1, endRow: 10, startColumn: 1, endColumn: 2 },
|
||||
DateSystem.Date1904
|
||||
);
|
||||
expect(zebraCrossingCacheController.updateZebraCrossingCache).toHaveBeenCalledWith('u1', 's1');
|
||||
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 4 }, vi.fn(() => false))).toBe(true);
|
||||
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's2', row: 4 }, vi.fn(() => false))).toBe(false);
|
||||
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 9 }, vi.fn(() => false))).toBe(false);
|
||||
|
||||
tableInitStatus$.next(true);
|
||||
workbookType$.next(workbook);
|
||||
activeSheet$.next({ getSheetId: () => 's1' });
|
||||
|
||||
expect(rowFilteredHandler(false, { unitId: 'u1', subUnitId: 's1', row: 8 }, vi.fn(() => false))).toBe(true);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { tableDefaultBorderStyle } from '../../config/config';
|
||||
import { customEmptyThemeWithBorderStyle, processStyleWithBorderStyle, tableThemeConfig } from '../table-theme.factory';
|
||||
|
||||
describe('table-theme.factory', () => {
|
||||
it('should fill border style for specific theme keys when border is missing', () => {
|
||||
expect(processStyleWithBorderStyle('headerRowStyle', {} as any)).toEqual({
|
||||
bd: { t: tableDefaultBorderStyle },
|
||||
});
|
||||
|
||||
expect(processStyleWithBorderStyle('lastRowStyle', {} as any)).toEqual({
|
||||
bd: { b: tableDefaultBorderStyle },
|
||||
});
|
||||
|
||||
expect(processStyleWithBorderStyle('lastColumnStyle', {} as any)).toEqual({
|
||||
bd: { r: tableDefaultBorderStyle },
|
||||
});
|
||||
|
||||
expect(processStyleWithBorderStyle('headerColumnStyle', {} as any)).toEqual({
|
||||
bd: { l: tableDefaultBorderStyle },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not override an existing border definition', () => {
|
||||
const style = { bd: { t: { s: 0 } }, bg: { rgb: '#fff' } } as any;
|
||||
expect(processStyleWithBorderStyle('headerRowStyle', style)).toBe(style);
|
||||
});
|
||||
|
||||
it('should expose the default empty theme with four-side border', () => {
|
||||
expect(customEmptyThemeWithBorderStyle.headerRowStyle?.bd?.t).toEqual(tableDefaultBorderStyle);
|
||||
expect(customEmptyThemeWithBorderStyle.headerColumnStyle?.bd?.l).toEqual(tableDefaultBorderStyle);
|
||||
expect(customEmptyThemeWithBorderStyle.lastColumnStyle?.bd?.r).toEqual(tableDefaultBorderStyle);
|
||||
expect(customEmptyThemeWithBorderStyle.lastRowStyle?.bd?.b).toEqual(tableDefaultBorderStyle);
|
||||
});
|
||||
|
||||
it('should generate built-in table themes with stable naming and border defaults', () => {
|
||||
expect(tableThemeConfig).toHaveLength(6);
|
||||
expect(tableThemeConfig[0].name).toBe('table-default-0');
|
||||
expect(tableThemeConfig[5].name).toBe('table-default-5');
|
||||
expect(tableThemeConfig[0].style.headerRowStyle?.bd?.t).toEqual(tableDefaultBorderStyle);
|
||||
expect(tableThemeConfig[0].style.lastColumnStyle?.bd?.r).toEqual(tableDefaultBorderStyle);
|
||||
expect(tableThemeConfig[0].style.secondRowStyle?.bg?.rgb).toBe('#EEF2FF');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRemoveSuperTableMutationParam, ISetSuperTableMutationParam, ISetSuperTableMutationSearchParam } from '@univerjs/engine-formula';
|
||||
import type { Table } from '../models/table';
|
||||
import { Disposable, ICommandService, Inject } from '@univerjs/core';
|
||||
import { RemoveSuperTableMutation, SetSuperTableMutation } from '@univerjs/engine-formula';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
|
||||
export class SheetTableFormulaController extends Disposable {
|
||||
constructor(
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
this._initRangeListener();
|
||||
}
|
||||
|
||||
private _initRangeListener() {
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableRangeChanged$.subscribe((event) => {
|
||||
const { tableId, unitId } = event;
|
||||
const table = this._tableManager.getTableById(unitId, tableId);
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
this._updateSuperTable(unitId, table);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableAdd$.subscribe((event) => {
|
||||
const { tableId, unitId } = event;
|
||||
const table = this._tableManager.getTableById(unitId, tableId);
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
this._updateSuperTable(unitId, table);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableDelete$.subscribe((event) => {
|
||||
const { unitId, subUnitId, tableName, range } = event;
|
||||
this._commandService.executeCommand<IRemoveSuperTableMutationParam>(RemoveSuperTableMutation.id, {
|
||||
unitId,
|
||||
tableName,
|
||||
reference: {
|
||||
sheetId: subUnitId,
|
||||
range,
|
||||
},
|
||||
}, { onlyLocal: true });
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableNameChanged$.subscribe((event) => {
|
||||
const { tableId, unitId, oldTableName } = event;
|
||||
|
||||
this._commandService.executeCommand<ISetSuperTableMutationSearchParam>(RemoveSuperTableMutation.id, {
|
||||
unitId,
|
||||
tableName: oldTableName,
|
||||
}, { onlyLocal: true });
|
||||
|
||||
const table = this._tableManager.getTableById(unitId, tableId);
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
this._updateSuperTable(unitId, table, oldTableName);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSuperTable(unitId: string, table: Table, oldTableName?: string) {
|
||||
const tableInfo = table.getTableInfo();
|
||||
const name = tableInfo.name;
|
||||
const columns = tableInfo.columns;
|
||||
const titleMap = new Map<string, number>();
|
||||
columns.forEach((column, index) => {
|
||||
titleMap.set(column.displayName, index);
|
||||
});
|
||||
this._commandService.executeCommand<ISetSuperTableMutationParam>(SetSuperTableMutation.id, {
|
||||
unitId,
|
||||
tableName: name,
|
||||
oldTableName,
|
||||
reference: {
|
||||
range: tableInfo.range,
|
||||
sheetId: tableInfo.subUnitId,
|
||||
titleMap,
|
||||
},
|
||||
}, { onlyLocal: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { IExclusiveRangeService } from '@univerjs/sheets';
|
||||
import { FEATURE_TABLE_ID } from '../const';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
|
||||
export class SheetTableRangeController extends Disposable {
|
||||
constructor(
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@Inject(IExclusiveRangeService) private _exclusiveRangeService: IExclusiveRangeService
|
||||
) {
|
||||
super();
|
||||
this._initRangeListener();
|
||||
}
|
||||
|
||||
private _initRangeListener() {
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableRangeChanged$.subscribe((event) => {
|
||||
const { range, tableId, unitId, subUnitId } = event;
|
||||
this._exclusiveRangeService.clearExclusiveRangesByGroupId(unitId, subUnitId, FEATURE_TABLE_ID, tableId);
|
||||
this._exclusiveRangeService.addExclusiveRange(unitId, subUnitId, FEATURE_TABLE_ID, [{
|
||||
range: { ...range },
|
||||
groupId: tableId,
|
||||
}]);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableAdd$.subscribe((event) => {
|
||||
const { tableId, unitId, subUnitId, range } = event;
|
||||
this._exclusiveRangeService.addExclusiveRange(unitId, subUnitId, FEATURE_TABLE_ID, [{
|
||||
range: { ...range },
|
||||
groupId: tableId,
|
||||
}]);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableDelete$.subscribe((event) => {
|
||||
const { tableId, unitId, subUnitId } = event;
|
||||
this._exclusiveRangeService.clearExclusiveRangesByGroupId(unitId, subUnitId, FEATURE_TABLE_ID, tableId);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMutationInfo, Workbook } from '@univerjs/core';
|
||||
import type {
|
||||
IInsertColCommandParams,
|
||||
IInsertRowCommandParams,
|
||||
IInsertRowMutationParams,
|
||||
IRemoveRowColCommandParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import type { ITableColumnJson, ITableJson } from '../types/type';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
Rectangle,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
InsertColCommand,
|
||||
InsertColMutation,
|
||||
InsertRowCommand,
|
||||
InsertRowMutation,
|
||||
RefRangeService,
|
||||
RemoveColCommand,
|
||||
RemoveColMutation,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { AddSheetTableMutation } from '../commands/mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../commands/mutations/delete-sheet-table.mutation';
|
||||
import { SetSheetTableMutation } from '../commands/mutations/set-sheet-table.mutation';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
import { IRangeOperationTypeEnum, IRowColTypeEnum } from '../types/type';
|
||||
import { convertCellDataToString, getColumnName } from '../util';
|
||||
|
||||
const SHEET_TABLE_REMOVE_COL_COMMAND_ID = 'sheet.command.table-remove-col';
|
||||
const DELETE_SHEET_TABLE_COMMAND_ID = 'sheet.command.delete-table';
|
||||
|
||||
export class SheetTableRefRangeController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ICommandService) private readonly _commandService: ICommandService,
|
||||
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@Inject(LocaleService) private _localeService: LocaleService
|
||||
|
||||
) {
|
||||
super();
|
||||
this._initCommandInterceptor();
|
||||
this._initCommandListener();
|
||||
}
|
||||
|
||||
private _initCommandInterceptor() {
|
||||
const self = this;
|
||||
this._sheetInterceptorService.interceptCommand({
|
||||
priority: -1,
|
||||
getMutations(commandInfo) {
|
||||
const defaultReturn = { redos: [], undos: [] };
|
||||
const { id, params } = commandInfo;
|
||||
switch (id) {
|
||||
case InsertRowCommand.id:
|
||||
return self._generateTableMutationWithInsertRow(params as IInsertRowCommandParams);
|
||||
case InsertColCommand.id:
|
||||
return self._generateTableMutationWithInsertCol(params as IInsertColCommandParams);
|
||||
case RemoveRowCommand.id:
|
||||
return self._generateTableMutationWithRemoveRow(params as IRemoveRowColCommandParams);
|
||||
case RemoveColCommand.id:
|
||||
return self._generateTableMutationWithRemoveCol(params as IRemoveRowColCommandParams);
|
||||
}
|
||||
|
||||
return defaultReturn;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _generateTableMutationWithInsertRow(insertParams: IInsertRowCommandParams) {
|
||||
const undos: IMutationInfo[] = [];
|
||||
const redos: IMutationInfo[] = [];
|
||||
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, insertParams);
|
||||
if (!target) {
|
||||
return { undos, redos };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
if (!allSubUnitTables.length) {
|
||||
return { undos, redos };
|
||||
}
|
||||
|
||||
const { range } = insertParams;
|
||||
allSubUnitTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startRow > tableRange.startRow && range.startRow <= tableRange.endRow) {
|
||||
const insertRowCount = range.endRow - range.startRow + 1;
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
endRow: tableRange.endRow + insertRowCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { undos, redos };
|
||||
}
|
||||
|
||||
private _generateTableMutationWithInsertCol(insertParams: IInsertColCommandParams) {
|
||||
const undos: IMutationInfo[] = [];
|
||||
const redos: IMutationInfo[] = [];
|
||||
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, insertParams);
|
||||
if (!target) {
|
||||
return { undos, redos };
|
||||
}
|
||||
const { unitId, subUnitId } = target;
|
||||
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
if (!allSubUnitTables.length) {
|
||||
return { undos, redos };
|
||||
}
|
||||
const { range } = insertParams;
|
||||
allSubUnitTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startColumn > tableRange.startColumn && range.startColumn <= tableRange.endColumn) {
|
||||
const insertColCount = range.endColumn - range.startColumn + 1;
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Insert,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: insertColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { undos, redos };
|
||||
}
|
||||
|
||||
private _generateTableMutationWithRemoveRow(removeParams: IRemoveRowColCommandParams) {
|
||||
const undos: IMutationInfo[] = [];
|
||||
const redos: IMutationInfo[] = [];
|
||||
const preRedos: IMutationInfo[] = [];
|
||||
const preUndos: IMutationInfo[] = [];
|
||||
|
||||
const target = getSheetCommandTarget(this._univerInstanceService);
|
||||
if (!target) {
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
const { unitId, subUnitId } = target;
|
||||
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
if (!allSubUnitTables.length) {
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
const { range } = removeParams;
|
||||
const removeRowCount = range.endRow - range.startRow + 1;
|
||||
|
||||
allSubUnitTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (Rectangle.intersects(tableRange, range)) {
|
||||
if (range.startRow <= tableRange.startRow && range.endRow >= tableRange.startRow) {
|
||||
preRedos.push({
|
||||
id: DeleteSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
},
|
||||
});
|
||||
const tableJson = table.toJSON();
|
||||
undos.push({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: tableJson.id,
|
||||
name: tableJson.name,
|
||||
range: tableJson.range,
|
||||
options: tableJson.options,
|
||||
},
|
||||
});
|
||||
} else if (range.startRow > tableRange.startRow && range.startRow <= tableRange.endRow) {
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
endRow: tableRange.endRow - removeRowCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (range.startRow < tableRange.endRow && range.endRow >= tableRange.endRow) {
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
endRow: range.startRow - 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
updateRange: {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
|
||||
private _generateTableMutationWithRemoveCol(removeParams: IRemoveRowColCommandParams) {
|
||||
const undos: IMutationInfo[] = [];
|
||||
const redos: IMutationInfo[] = [];
|
||||
const preRedos: IMutationInfo[] = [];
|
||||
const preUndos: IMutationInfo[] = [];
|
||||
|
||||
const target = getSheetCommandTarget(this._univerInstanceService);
|
||||
if (!target) {
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
const { unitId, subUnitId } = target;
|
||||
const allSubUnitTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
if (!allSubUnitTables.length) {
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
const { range } = removeParams;
|
||||
const removeColCount = range.endColumn - range.startColumn + 1;
|
||||
allSubUnitTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (Rectangle.intersects(tableRange, range)) {
|
||||
if (range.startColumn <= tableRange.startColumn && range.endColumn >= tableRange.endColumn) {
|
||||
const tableInfo = table.getTableInfo();
|
||||
const formulaMutations = this._sheetInterceptorService.onCommandExecute({
|
||||
id: DELETE_SHEET_TABLE_COMMAND_ID,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
tableName: tableInfo.name,
|
||||
},
|
||||
});
|
||||
preRedos.push(...(formulaMutations.preRedos ?? []), ...formulaMutations.redos);
|
||||
preRedos.push({
|
||||
id: DeleteSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
},
|
||||
});
|
||||
const tableJson = table.toJSON();
|
||||
const { startRow, startColumn, endColumn } = tableJson.range;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
const header: string[] = [];
|
||||
for (let i = startColumn; i <= endColumn; i++) {
|
||||
header.push(convertCellDataToString(worksheet?.getCell(startRow, i)) || getColumnName(i - startColumn + 1, this._localeService.t<LocaleKey>('sheets-table.columnPrefix')));
|
||||
}
|
||||
undos.push({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: tableJson.id,
|
||||
name: tableJson.name,
|
||||
header,
|
||||
range: tableJson.range,
|
||||
options: tableJson.options,
|
||||
},
|
||||
});
|
||||
undos.push(...(formulaMutations.preUndos ?? []), ...formulaMutations.undos);
|
||||
} else if (range.startColumn <= tableRange.startColumn && range.endColumn >= tableRange.startColumn) {
|
||||
const tableJson = table.toJSON();
|
||||
const removeColumnCount = range.endColumn - tableRange.startColumn + 1;
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: tableRange.startColumn,
|
||||
count: removeColumnCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const columns: ITableColumnJson[] = [];
|
||||
for (let i = 0; i < removeColumnCount; i++) {
|
||||
const column = table.getTableColumnByIndex(i);
|
||||
if (column) {
|
||||
columns.push(column.toJSON());
|
||||
}
|
||||
}
|
||||
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
|
||||
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
|
||||
this._appendTableColumnFormulaMutations(redos, undos, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
tableName: table.getTableInfo().name,
|
||||
range,
|
||||
columns,
|
||||
});
|
||||
} else if (range.startColumn > tableRange.startColumn && range.endColumn > tableRange.endColumn) {
|
||||
const tableJson = table.toJSON();
|
||||
const removeColumnCount = tableRange.endColumn - range.startColumn + 1;
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: removeColumnCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ITableColumnJson[] = [];
|
||||
const gap = range.startColumn - tableRange.startColumn;
|
||||
for (let i = 0; i < removeColumnCount; i++) {
|
||||
const column = table.getTableColumnByIndex(i + gap);
|
||||
if (column) {
|
||||
columns.push(column.toJSON());
|
||||
}
|
||||
}
|
||||
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
|
||||
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
|
||||
this._appendTableColumnFormulaMutations(redos, undos, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
tableName: table.getTableInfo().name,
|
||||
range,
|
||||
columns,
|
||||
});
|
||||
} else if (range.startColumn > tableRange.startColumn && range.endColumn <= tableRange.endColumn) {
|
||||
const tableJson = table.toJSON();
|
||||
redos.push({
|
||||
id: SetSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
config: {
|
||||
rowColOperation: {
|
||||
operationType: IRangeOperationTypeEnum.Delete,
|
||||
rowColType: IRowColTypeEnum.Col,
|
||||
index: range.startColumn,
|
||||
count: removeColCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ITableColumnJson[] = [];
|
||||
const gap = range.startColumn - tableRange.startColumn;
|
||||
for (let i = 0; i < removeColCount; i++) {
|
||||
const column = table.getTableColumnByIndex(i + gap);
|
||||
if (column) {
|
||||
columns.push(column.toJSON());
|
||||
}
|
||||
}
|
||||
preUndos.push(this._getDeleteTableMutation(unitId, subUnitId, table.getId()));
|
||||
undos.push(this._getAddTableMutation(unitId, subUnitId, tableJson));
|
||||
this._appendTableColumnFormulaMutations(redos, undos, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: table.getId(),
|
||||
tableName: table.getTableInfo().name,
|
||||
range,
|
||||
columns,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { undos, redos, preRedos, preUndos };
|
||||
}
|
||||
|
||||
private _getDeleteTableMutation(unitId: string, subUnitId: string, tableId: string): IMutationInfo {
|
||||
return {
|
||||
id: DeleteSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _getAddTableMutation(unitId: string, subUnitId: string, tableJson: ITableJson): IMutationInfo {
|
||||
const header = tableJson.columns.map((column) => column.displayName);
|
||||
|
||||
return {
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: tableJson.id,
|
||||
name: tableJson.name,
|
||||
header,
|
||||
range: tableJson.range,
|
||||
options: {
|
||||
...tableJson.options,
|
||||
columns: tableJson.columns,
|
||||
filters: tableJson.filters.tableColumnFilterList,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _appendTableColumnFormulaMutations(
|
||||
redos: IMutationInfo[],
|
||||
undos: IMutationInfo[],
|
||||
info: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
tableName: string;
|
||||
range: IRemoveRowColCommandParams['range'];
|
||||
columns: ITableColumnJson[];
|
||||
}
|
||||
) {
|
||||
const removedColumnNames = info.columns.map((column) => column.displayName);
|
||||
if (!removedColumnNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formulaMutations = this._sheetInterceptorService.onCommandExecute({
|
||||
id: SHEET_TABLE_REMOVE_COL_COMMAND_ID,
|
||||
params: {
|
||||
unitId: info.unitId,
|
||||
subUnitId: info.subUnitId,
|
||||
tableId: info.tableId,
|
||||
tableName: info.tableName,
|
||||
range: info.range,
|
||||
removedColumnNames,
|
||||
},
|
||||
});
|
||||
|
||||
redos.splice(Math.max(redos.length - 1, 0), 0, ...(formulaMutations.preRedos ?? []), ...formulaMutations.redos);
|
||||
undos.push(...(formulaMutations.preUndos ?? []), ...formulaMutations.undos);
|
||||
}
|
||||
|
||||
private _initCommandListener() {
|
||||
this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === InsertRowMutation.id) {
|
||||
const params = commandInfo.params as IInsertRowMutationParams;
|
||||
const { unitId, subUnitId, range } = params;
|
||||
const insertCount = range.endRow - range.startRow + 1;
|
||||
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
allTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startRow <= tableRange.startRow) {
|
||||
this._tableManager.updateTableRange(unitId, table.getId(), {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
startRow: tableRange.startRow + insertCount,
|
||||
endRow: tableRange.endRow + insertCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (commandInfo.id === InsertColMutation.id) {
|
||||
const params = commandInfo.params as IInsertRowMutationParams;
|
||||
const { unitId, subUnitId, range } = params;
|
||||
const insertCount = range.endColumn - range.startColumn + 1;
|
||||
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
allTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startColumn <= tableRange.startColumn) {
|
||||
this._tableManager.updateTableRange(unitId, table.getId(), {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
startColumn: tableRange.startColumn + insertCount,
|
||||
endColumn: tableRange.endColumn + insertCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (commandInfo.id === RemoveRowMutation.id) {
|
||||
const params = commandInfo.params as IInsertRowMutationParams;
|
||||
const { unitId, subUnitId, range } = params;
|
||||
const removeCount = range.endRow - range.startRow + 1;
|
||||
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
allTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startRow < tableRange.startRow) {
|
||||
this._tableManager.updateTableRange(unitId, table.getId(), {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
startRow: tableRange.startRow - removeCount,
|
||||
endRow: tableRange.endRow - removeCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (commandInfo.id === RemoveColMutation.id) {
|
||||
const params = commandInfo.params as IInsertRowMutationParams;
|
||||
const { unitId, subUnitId, range } = params;
|
||||
const removeCount = range.endColumn - range.startColumn + 1;
|
||||
const allTables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
allTables.forEach((table) => {
|
||||
const tableRange = table.getRange();
|
||||
if (range.startColumn < tableRange.startColumn) {
|
||||
this._tableManager.updateTableRange(unitId, table.getId(), {
|
||||
newRange: {
|
||||
...tableRange,
|
||||
startColumn: tableRange.startColumn - removeCount,
|
||||
endColumn: tableRange.endColumn - removeCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ITableDefaultThemeStyle, IUniverSheetsTableConfig } from '../config/config';
|
||||
import { Disposable, IConfigService, Inject } from '@univerjs/core';
|
||||
import { RangeThemeStyle, SheetRangeThemeModel, SheetRangeThemeService } from '@univerjs/sheets';
|
||||
import { SHEETS_TABLE_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
import { tableThemeConfig } from './table-theme.factory';
|
||||
|
||||
export class SheetsTableThemeController extends Disposable {
|
||||
private _defaultThemeIndex: number = 0;
|
||||
private _allThemes: ITableDefaultThemeStyle[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@Inject(SheetRangeThemeService) private _sheetRangeThemeService: SheetRangeThemeService,
|
||||
@Inject(SheetRangeThemeModel) private _sheetRangeThemeModel: SheetRangeThemeModel,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
this._initUserTableTheme();
|
||||
this.registerTableChangeEvent();
|
||||
this._initDefaultTableTheme();
|
||||
}
|
||||
|
||||
registerTableChangeEvent() {
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableAdd$.subscribe((event) => {
|
||||
const { range, tableId, unitId, subUnitId, tableStyleId } = event;
|
||||
const table = this._tableManager.getTable(unitId, tableId)!;
|
||||
// If the tableStyleId is not set or is empty string, use the default theme
|
||||
const _tableStyleId = tableStyleId || this._allThemes[this._defaultThemeIndex].name;
|
||||
table.setTableStyleId(_tableStyleId);
|
||||
this._sheetRangeThemeService.registerRangeThemeStyle(_tableStyleId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableRangeChanged$.subscribe((event) => {
|
||||
const { range, oldRange, tableId, unitId, subUnitId } = event;
|
||||
const table = this._tableManager.getTable(unitId, tableId)!;
|
||||
let tableStyleId = table.getTableStyleId();
|
||||
if (!tableStyleId) {
|
||||
tableStyleId = this._allThemes[this._defaultThemeIndex].name;
|
||||
table.setTableStyleId(tableStyleId);
|
||||
}
|
||||
this._sheetRangeThemeService.removeRangeThemeRule(tableStyleId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...oldRange },
|
||||
});
|
||||
this._sheetRangeThemeService.registerRangeThemeStyle(tableStyleId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableThemeChanged$.subscribe((event) => {
|
||||
const { theme, oldTheme, tableId, unitId, subUnitId } = event;
|
||||
const table = this._tableManager.getTable(unitId, tableId)!;
|
||||
const range = table.getRange();
|
||||
this._sheetRangeThemeService.removeRangeThemeRule(oldTheme, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
});
|
||||
this._sheetRangeThemeService.registerRangeThemeStyle(theme, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableDelete$.subscribe((event) => {
|
||||
const { range, unitId, subUnitId, tableStyleId = this._allThemes[this._defaultThemeIndex].name } = event;
|
||||
this._sheetRangeThemeService.removeRangeThemeRule(tableStyleId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initUserTableTheme() {
|
||||
const tableConfig: IUniverSheetsTableConfig = this._configService.getConfig(SHEETS_TABLE_PLUGIN_CONFIG_KEY) || {};
|
||||
|
||||
const defaultThemeIndex = tableConfig.defaultThemeIndex || 0;
|
||||
const userThemes = tableConfig.userThemes || [];
|
||||
|
||||
this._defaultThemeIndex = defaultThemeIndex;
|
||||
this._allThemes = userThemes.concat(tableThemeConfig);
|
||||
}
|
||||
|
||||
private _initDefaultTableTheme() {
|
||||
for (let i = 0; i < this._allThemes.length; i++) {
|
||||
const { name, style } = this._allThemes[i];
|
||||
const rangeThemeStyle = new RangeThemeStyle(name, style);
|
||||
this._sheetRangeThemeModel.registerDefaultRangeTheme(rangeThemeStyle);
|
||||
}
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this._allThemes = [];
|
||||
this._defaultThemeIndex = 0;
|
||||
}
|
||||
}
|
||||
299
packages/sheets-table/src/controllers/sheets-table.controller.ts
Normal file
299
packages/sheets-table/src/controllers/sheets-table.controller.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMutationInfo, IRange, Workbook } from '@univerjs/core';
|
||||
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
|
||||
import type { ITableResource } from '../types/type';
|
||||
import { Disposable, generateRandomId, Inject, InterceptorEffectEnum, IResourceManagerService, IUniverInstanceService, Rectangle, RTree, UniverInstanceType } from '@univerjs/core';
|
||||
import { CopySheetCommand, INTERCEPTOR_POINT, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { AddSheetTableMutation } from '../commands/mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from '../commands/mutations/delete-sheet-table.mutation';
|
||||
import { PLUGIN_NAME } from '../const';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
|
||||
export class SheetsTableController extends Disposable {
|
||||
private _tableRangeRTree = new Map<string, RTree>();
|
||||
constructor(
|
||||
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@Inject(IResourceManagerService) private _resourceManagerService: IResourceManagerService
|
||||
) {
|
||||
super();
|
||||
this._initSnapshot();
|
||||
this._initSheetChange();
|
||||
this.registerTableChangeEvent();
|
||||
this.registerTableHeaderInterceptor();
|
||||
}
|
||||
|
||||
getContainerTableWithRange(unitId: string, subUnitId: string, range: IRange) {
|
||||
const rTree = this._ensureTableRangeRTree(unitId);
|
||||
const ids = Array.from(rTree.bulkSearch([{ unitId, sheetId: subUnitId, range }]));
|
||||
const wrapperTableId = ids.find((id) => {
|
||||
const table = this._tableManager.getTable(unitId, String(id));
|
||||
if (table) {
|
||||
return Rectangle.contains(table.getRange(), range);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (wrapperTableId) {
|
||||
const table = this._tableManager.getTable(unitId, String(wrapperTableId));
|
||||
return table;
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureTableRangeRTree(unitId: string) {
|
||||
if (!this._tableRangeRTree.has(unitId)) {
|
||||
this._tableRangeRTree.set(unitId, new RTree());
|
||||
}
|
||||
return this._tableRangeRTree.get(unitId)!;
|
||||
}
|
||||
|
||||
registerTableChangeEvent() {
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableAdd$.subscribe((event) => {
|
||||
const { range, tableId, unitId, subUnitId } = event;
|
||||
const rTree = this._ensureTableRangeRTree(unitId);
|
||||
rTree.insert({
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
id: tableId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableRangeChanged$.subscribe((event) => {
|
||||
const { range, tableId, unitId, subUnitId, oldRange } = event;
|
||||
const rTree = this._ensureTableRangeRTree(unitId);
|
||||
rTree.remove({
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
id: tableId,
|
||||
range: { ...oldRange },
|
||||
});
|
||||
rTree.insert({
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
id: tableId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableDelete$.subscribe((event) => {
|
||||
const { tableId, unitId, subUnitId, range } = event;
|
||||
const rTree = this._ensureTableRangeRTree(unitId);
|
||||
rTree.remove({
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
id: tableId,
|
||||
range: { ...range },
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registerTableHeaderInterceptor() {
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, {
|
||||
effect: InterceptorEffectEnum.Value,
|
||||
handler: (cell, context, next) => {
|
||||
const { row, col, unitId, subUnitId } = context;
|
||||
const rTree = this._ensureTableRangeRTree(unitId);
|
||||
const v = cell?.v;
|
||||
if (v === undefined && rTree) {
|
||||
const ids = Array.from(rTree.bulkSearch([{ unitId, sheetId: subUnitId, range: { startColumn: col, endColumn: col, startRow: row, endRow: row } }]));
|
||||
if (ids.length > 0) {
|
||||
const table = this._tableManager.getTable(unitId, ids[0] as string);
|
||||
if (table) {
|
||||
const tableRange = table.getRange();
|
||||
const index = col - tableRange.startColumn;
|
||||
const tableStartRow = tableRange.startRow;
|
||||
if (tableStartRow === row) {
|
||||
const columnName = table.getColumnNameByIndex(index);
|
||||
if (!cell || cell === context.rawData) {
|
||||
cell = { ...context.rawData };
|
||||
}
|
||||
|
||||
cell.v = columnName;
|
||||
return next(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return next(cell);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _toJson(unitId: string): ITableResource {
|
||||
return this._tableManager.toJSON(unitId);
|
||||
}
|
||||
|
||||
private _fromJSON(unitId: string, resources: ITableResource) {
|
||||
return this._tableManager.fromJSON(unitId, resources);
|
||||
}
|
||||
|
||||
private _deleteUnitId(unitId: string) {
|
||||
this._tableManager.deleteUnitId(unitId);
|
||||
}
|
||||
|
||||
private _initSnapshot(): void {
|
||||
this.disposeWithMe(this._resourceManagerService.registerPluginResource({
|
||||
toJson: (unitId: string) => {
|
||||
return JSON.stringify(this._toJson(unitId));
|
||||
},
|
||||
parseJson: (json: string) => {
|
||||
if (!json) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
businesses: [UniverInstanceType.UNIVER_SHEET],
|
||||
pluginName: PLUGIN_NAME,
|
||||
onLoad: (unitId, resources) => {
|
||||
this._fromJSON(unitId, resources);
|
||||
},
|
||||
onUnLoad: (unitId) => {
|
||||
this._deleteUnitId(unitId);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initSheetChange() {
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.interceptCommand({
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
getMutations: (commandInfo) => {
|
||||
if (commandInfo.id === RemoveSheetCommand.id) {
|
||||
const params = commandInfo.params as IRemoveSheetCommandParams;
|
||||
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
|
||||
const subUnitId = params.subUnitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
|
||||
if (tables.length === 0) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
tables.forEach((table) => {
|
||||
const tableJson = table.toJSON();
|
||||
|
||||
redos.push({
|
||||
id: DeleteSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: tableJson.id,
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
name: tableJson.name,
|
||||
range: tableJson.range,
|
||||
tableId: tableJson.id,
|
||||
options: {
|
||||
...tableJson.options,
|
||||
columns: tableJson.columns,
|
||||
filters: tableJson.filters.tableColumnFilterList,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
} else if (commandInfo.id === CopySheetCommand.id) {
|
||||
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
|
||||
const { unitId, subUnitId, targetSubUnitId } = params;
|
||||
|
||||
if (!unitId || !subUnitId || !targetSubUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
|
||||
if (tables.length === 0) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
tables.forEach((table) => {
|
||||
const tableJson = table.toJSON();
|
||||
const tableId = generateRandomId();
|
||||
|
||||
redos.push({
|
||||
id: AddSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
name: tableJson.name,
|
||||
range: {
|
||||
...tableJson.range,
|
||||
sheetId: targetSubUnitId,
|
||||
},
|
||||
tableId,
|
||||
options: {
|
||||
...tableJson.options,
|
||||
columns: tableJson.columns,
|
||||
filters: tableJson.filters.tableColumnFilterList,
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: DeleteSheetTableMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
tableId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
return { redos: [], undos: [] };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this._tableRangeRTree.clear();
|
||||
}
|
||||
}
|
||||
120
packages/sheets-table/src/controllers/table-filter.controller.ts
Normal file
120
packages/sheets-table/src/controllers/table-filter.controller.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, INTERCEPTOR_POINT, SheetInterceptorService, ZebraCrossingCacheController } from '@univerjs/sheets';
|
||||
import { filter, switchMap } from 'rxjs';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
|
||||
export class TableFilterController extends Disposable {
|
||||
private readonly _tableFilteredOutRows = new Map<string, Set<number>>();
|
||||
|
||||
private _subscription: Subscription | null = null;
|
||||
constructor(
|
||||
@Inject(TableManager) private _tableManager: TableManager,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(ZebraCrossingCacheController) private readonly _zebraCrossingCacheController: ZebraCrossingCacheController
|
||||
) {
|
||||
super();
|
||||
this.registerFilterChangeEvent();
|
||||
this.initTableHiddenRowIntercept();
|
||||
this._initFilteredOutRows();
|
||||
}
|
||||
|
||||
initTableHiddenRowIntercept() {
|
||||
this.disposeWithMe(this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.ROW_FILTERED, {
|
||||
// Priority must be higher than sheet filtering, because filtering has no next, and those below filtering will not trigger interceptor
|
||||
priority: 100,
|
||||
handler: (filtered, rowLocation, next) => {
|
||||
if (filtered) return true;
|
||||
const isTableFiltered = this._getTableFilteredOutRows(rowLocation.unitId, rowLocation.subUnitId).has(rowLocation.row);
|
||||
return isTableFiltered ? true : next(isTableFiltered);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _initFilteredOutRows() {
|
||||
this._tableManager.tableInitStatus$.pipe(
|
||||
filter((initialized) => initialized),
|
||||
switchMap(() => {
|
||||
return this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
}),
|
||||
filter((workbook) => workbook !== null && workbook !== undefined),
|
||||
switchMap((workbook) => workbook.activeSheet$),
|
||||
filter((sheet) => sheet !== null && sheet !== undefined)
|
||||
).subscribe(() => {
|
||||
const target = getSheetCommandTarget(this._univerInstanceService);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const { unitId, subUnitId } = target;
|
||||
this._refreshTableFilteredOutRows(unitId, subUnitId);
|
||||
});
|
||||
}
|
||||
|
||||
registerFilterChangeEvent() {
|
||||
this.disposeWithMe(
|
||||
this._tableManager.tableFilterChanged$.subscribe((event) => {
|
||||
const { unitId, subUnitId, tableId } = event;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
const table = this._tableManager.getTable(unitId, tableId);
|
||||
if (!worksheet || !table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tableFilter = table.getTableFilters();
|
||||
tableFilter.doFilter(worksheet, table.getTableFilterRange(), workbook?.getDateSystem());
|
||||
|
||||
this._refreshTableFilteredOutRows(unitId, subUnitId);
|
||||
|
||||
this._zebraCrossingCacheController.updateZebraCrossingCache(unitId, subUnitId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _refreshTableFilteredOutRows(unitId: string, subUnitId: string): void {
|
||||
const filteredOutRows = new Set<number>();
|
||||
const tables = this._tableManager.getTablesBySubunitId(unitId, subUnitId);
|
||||
tables.forEach((table) => {
|
||||
const tableFilteredRows = table.getTableFilters().getFilterOutRows();
|
||||
if (!tableFilteredRows) {
|
||||
return;
|
||||
}
|
||||
for (const row of tableFilteredRows) {
|
||||
filteredOutRows.add(row);
|
||||
}
|
||||
});
|
||||
|
||||
this._tableFilteredOutRows.set(this._getSheetKey(unitId, subUnitId), filteredOutRows);
|
||||
}
|
||||
|
||||
private _getTableFilteredOutRows(unitId: string, subUnitId: string): Set<number> {
|
||||
return this._tableFilteredOutRows.get(this._getSheetKey(unitId, subUnitId)) ?? new Set();
|
||||
}
|
||||
|
||||
private _getSheetKey(unitId: string, subUnitId: string): string {
|
||||
return `${unitId}|${subUnitId}`;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._subscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
142
packages/sheets-table/src/controllers/table-theme.factory.ts
Normal file
142
packages/sheets-table/src/controllers/table-theme.factory.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRangeThemeStyleItem, IRangeThemeStyleJSON } from '@univerjs/sheets';
|
||||
import type { ITableDefaultThemeStyle } from '../config/config';
|
||||
import { tableDefaultBorderStyle } from '../config/config';
|
||||
|
||||
export const customEmptyThemeWithBorderStyle: Omit<IRangeThemeStyleJSON, 'name'> = {
|
||||
headerRowStyle: {
|
||||
bd: {
|
||||
t: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
headerColumnStyle: {
|
||||
bd: {
|
||||
l: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
lastColumnStyle: {
|
||||
bd: {
|
||||
r: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
lastRowStyle: {
|
||||
bd: {
|
||||
b: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const processStyleWithBorderStyle = (key: keyof Omit<IRangeThemeStyleJSON, 'name'>, style: IRangeThemeStyleItem) => {
|
||||
if (key === 'headerRowStyle') {
|
||||
if (!style.bd) {
|
||||
return {
|
||||
...style,
|
||||
bd: {
|
||||
t: tableDefaultBorderStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (key === 'lastRowStyle') {
|
||||
if (!style.bd) {
|
||||
return {
|
||||
...style,
|
||||
bd: {
|
||||
b: tableDefaultBorderStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (key === 'lastColumnStyle') {
|
||||
if (!style.bd) {
|
||||
return {
|
||||
...style,
|
||||
bd: {
|
||||
r: tableDefaultBorderStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (key === 'headerColumnStyle') {
|
||||
if (!style.bd) {
|
||||
return {
|
||||
...style,
|
||||
bd: {
|
||||
l: tableDefaultBorderStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
const tableDefaultThemeStyleArr = [
|
||||
[['#6280F9', '#FFFFFF', '#EEF2FF', '#DCE4FF'], ['#fff']],
|
||||
[['#16BDCA', '#FFFFFF', '#EDFAFA', '#AFECEF'], ['#000']],
|
||||
[['#31C48D', '#FFFFFF', '#F3FAF7', '#BCF0DA'], ['#fff']],
|
||||
[['#AC94FA', '#FFFFFF', '#F6F5FF', '#EDEBFE'], ['#fff']],
|
||||
[['#F17EBB', '#FFFFFF', '#FDF2F8', '#FCE8F3'], ['#fff']],
|
||||
[['#F98080', '#FFFFFF', '#FDF2F2', '#FDE8E8'], ['#fff']],
|
||||
];
|
||||
|
||||
export const tableThemeConfig: ITableDefaultThemeStyle[] = tableDefaultThemeStyleArr.map((item, index) => {
|
||||
const [backgroundArr, colorArr] = item;
|
||||
const [headerRowBg, firstRowBg, secondRowBg, lastRowBg] = backgroundArr;
|
||||
const [headerCl] = colorArr;
|
||||
return {
|
||||
name: `table-default-${index}`,
|
||||
style: {
|
||||
headerRowStyle: {
|
||||
bg: {
|
||||
rgb: headerRowBg,
|
||||
},
|
||||
cl: {
|
||||
rgb: headerCl,
|
||||
},
|
||||
bd: {
|
||||
t: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
headerColumnStyle: {
|
||||
bd: {
|
||||
l: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
firstRowStyle: {
|
||||
bg: {
|
||||
rgb: firstRowBg,
|
||||
},
|
||||
},
|
||||
secondRowStyle: {
|
||||
bg: {
|
||||
rgb: secondRowBg,
|
||||
},
|
||||
},
|
||||
lastRowStyle: {
|
||||
bg: {
|
||||
rgb: lastRowBg,
|
||||
},
|
||||
bd: {
|
||||
b: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
lastColumnStyle: {
|
||||
bd: {
|
||||
r: tableDefaultBorderStyle,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
296
packages/sheets-table/src/facade/__tests__/f-table.spec.ts
Normal file
296
packages/sheets-table/src/facade/__tests__/f-table.spec.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 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, UnitModel } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import {
|
||||
DefinedNamesService,
|
||||
FormulaDataModel,
|
||||
FunctionService,
|
||||
IDefinedNamesService,
|
||||
IFunctionService,
|
||||
ISuperTableService,
|
||||
LexerTreeBuilder,
|
||||
SuperTableService,
|
||||
} from '@univerjs/engine-formula';
|
||||
import {
|
||||
AddRangeThemeMutation,
|
||||
RefRangeService,
|
||||
RemoveRangeThemeMutation,
|
||||
SheetInterceptorService,
|
||||
SheetRangeThemeModel,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
AddSheetTableCommand,
|
||||
AddSheetTableMutation,
|
||||
AddTableThemeCommand,
|
||||
DeleteSheetTableCommand,
|
||||
DeleteSheetTableMutation,
|
||||
SetSheetTableCommand,
|
||||
SetSheetTableFilterCommand,
|
||||
SetSheetTableFilterMutation,
|
||||
SetSheetTableMutation,
|
||||
SheetTableService,
|
||||
TableColumnFilterTypeEnum,
|
||||
TableConditionTypeEnum,
|
||||
TableManager,
|
||||
TableNumberCompareTypeEnum,
|
||||
} from '@univerjs/sheets-table';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import enUS from '../../locale/en-US';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-table/facade';
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {},
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createTableFacadeTestBed(dependencies: Dependency[]) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([SheetRangeThemeModel]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([RefRangeService]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
dependencies.forEach((dependency) => injector.add(dependency));
|
||||
|
||||
const localeService = injector.get(LocaleService);
|
||||
localeService.load({ [LocaleType.EN_US]: enUS });
|
||||
localeService.setLocale(LocaleType.EN_US);
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
|
||||
injector.get(IUniverInstanceService).focusUnit(sheet.getUnitId());
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
univerAPI: FUniver.newAPI(injector),
|
||||
};
|
||||
}
|
||||
|
||||
describe('sheets-table facade', () => {
|
||||
let testBed: ReturnType<typeof createTableFacadeTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
const dependencies: Dependency[] = [
|
||||
[TableManager],
|
||||
[SheetTableService],
|
||||
];
|
||||
|
||||
testBed = createTableFacadeTestBed(dependencies);
|
||||
|
||||
const commandService = testBed.get(ICommandService);
|
||||
[
|
||||
AddSheetTableCommand,
|
||||
AddSheetTableMutation,
|
||||
AddTableThemeCommand,
|
||||
AddRangeThemeMutation,
|
||||
DeleteSheetTableCommand,
|
||||
DeleteSheetTableMutation,
|
||||
RemoveRangeThemeMutation,
|
||||
SetSheetTableCommand,
|
||||
SetSheetTableMutation,
|
||||
SetSheetTableFilterCommand,
|
||||
SetSheetTableFilterMutation,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('manages worksheet tables through workbook and worksheet facades', async () => {
|
||||
const workbook = testBed.univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const range = worksheet.getRange('A1:B3').getRange();
|
||||
const sheetTableService = testBed.get(SheetTableService);
|
||||
|
||||
sheetTableService.addTable(
|
||||
workbook.getId(),
|
||||
worksheet.getSheetId(),
|
||||
'Orders',
|
||||
range,
|
||||
['Name', 'Quantity'],
|
||||
'table-orders',
|
||||
{ tableStyleId: 'table-default-1' }
|
||||
);
|
||||
expect(workbook.getTableInfo('table-orders')).toMatchObject({
|
||||
id: 'table-orders',
|
||||
name: 'Orders',
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
range,
|
||||
unitId: workbook.getId(),
|
||||
});
|
||||
expect(workbook.getTableInfoByName('Orders')?.id).toBe('table-orders');
|
||||
expect(worksheet.getSubTableInfos().map((table) => table.id)).toEqual(['table-orders']);
|
||||
expect(worksheet.getTableByCell(1, 1)?.id).toBe('table-orders');
|
||||
|
||||
await expect(worksheet.setTableName('table-orders', 'Orders2026')).resolves.toBe(true);
|
||||
expect(workbook.getTableInfoByName('Orders2026')?.id).toBe('table-orders');
|
||||
|
||||
await expect(workbook.removeTable('table-orders')).resolves.toBe(true);
|
||||
expect(workbook.getTableList()).toEqual([]);
|
||||
await expect(workbook.removeTable('missing-table')).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('sets and clears table filters and exposes table enums on the facade', async () => {
|
||||
const workbook = testBed.univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
testBed.get(SheetTableService).addTable(
|
||||
workbook.getId(),
|
||||
worksheet.getSheetId(),
|
||||
'Inventory',
|
||||
worksheet.getRange('A1:B3').getRange(),
|
||||
['Name', 'Quantity'],
|
||||
'table-inventory'
|
||||
);
|
||||
|
||||
expect(testBed.univerAPI.Enum.TableColumnFilterTypeEnum.condition).toBe(TableColumnFilterTypeEnum.condition);
|
||||
await expect(workbook.setTableFilter('table-inventory', 1, {
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.GreaterThan,
|
||||
expectedValue: 10,
|
||||
},
|
||||
})).resolves.toBe(true);
|
||||
await expect(worksheet.resetFilter('table-inventory', 1)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('adds and resizes tables through workbook and worksheet facades', async () => {
|
||||
const workbook = testBed.univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const invoiceRange = worksheet.getRange('A1:B3').getRange();
|
||||
|
||||
await expect(workbook.addTable(worksheet.getSheetId(), 'Invoices', invoiceRange, 'table-invoices', {
|
||||
tableStyleId: 'table-default-2',
|
||||
})).resolves.toBe('table-invoices');
|
||||
expect(workbook.getTableInfo('table-invoices')).toMatchObject({
|
||||
id: 'table-invoices',
|
||||
name: 'Invoices',
|
||||
range: invoiceRange,
|
||||
});
|
||||
|
||||
const resizedRange = worksheet.getRange('A1:B4').getRange();
|
||||
await expect(worksheet.setTableRange('table-invoices', resizedRange)).resolves.toBe(true);
|
||||
expect(workbook.getTableInfo('table-invoices')?.range).toEqual(resizedRange);
|
||||
expect(worksheet.setTableName('table-invoices', 'sheet1')).toBe(false);
|
||||
|
||||
await expect(workbook.removeTable('table-invoices')).resolves.toBe(true);
|
||||
|
||||
await expect(worksheet.addTable('Receipts', worksheet.getRange('E1:F3').getRange(), 'table-receipts')).resolves.toBe(true);
|
||||
expect(workbook.getTableInfoByName('Receipts')?.id).toBe('table-receipts');
|
||||
await expect(worksheet.removeTable('table-receipts')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('styles and removes tables through worksheet facade commands', async () => {
|
||||
const workbook = testBed.univerAPI.getActiveWorkbook()!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const range = worksheet.getRange('C1:D4').getRange();
|
||||
const sheetTableService = testBed.get(SheetTableService);
|
||||
const tableManager = testBed.get(TableManager);
|
||||
|
||||
sheetTableService.addTable(
|
||||
workbook.getId(),
|
||||
worksheet.getSheetId(),
|
||||
'Shipments',
|
||||
range,
|
||||
['Carrier', 'Count'],
|
||||
'table-shipments',
|
||||
{ tableStyleId: 'table-default-1' }
|
||||
);
|
||||
expect(workbook.getTableInfo('table-shipments')).toMatchObject({
|
||||
id: 'table-shipments',
|
||||
name: 'Shipments',
|
||||
range,
|
||||
});
|
||||
|
||||
await expect(worksheet.addTableTheme('table-shipments', {
|
||||
name: 'table-custom',
|
||||
headerRowStyle: {
|
||||
bg: { rgb: '#111111' },
|
||||
cl: { rgb: '#ffffff' },
|
||||
},
|
||||
firstRowStyle: {
|
||||
bg: { rgb: '#ffffff' },
|
||||
},
|
||||
secondRowStyle: {
|
||||
bg: { rgb: '#eeeeee' },
|
||||
},
|
||||
})).resolves.toBe(true);
|
||||
expect(tableManager.getTableById(workbook.getId(), 'table-shipments')?.getTableStyleId()).toBe('table-custom');
|
||||
|
||||
await expect(worksheet.removeTable('table-shipments')).resolves.toBe(true);
|
||||
expect(worksheet.getSubTableInfos()).toEqual([]);
|
||||
});
|
||||
});
|
||||
61
packages/sheets-table/src/facade/f-enum.ts
Normal file
61
packages/sheets-table/src/facade/f-enum.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { FEnum } from '@univerjs/core/facade';
|
||||
import { TableColumnFilterTypeEnum, TableConditionTypeEnum, TableDateCompareTypeEnum, TableNumberCompareTypeEnum, TableStringCompareTypeEnum } from '@univerjs/sheets-table';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFSheetsTableEnumMixin {
|
||||
/** Filter kinds available for table columns. */
|
||||
TableColumnFilterTypeEnum: typeof TableColumnFilterTypeEnum;
|
||||
/** Value types used by table condition filters. */
|
||||
TableConditionTypeEnum: typeof TableConditionTypeEnum;
|
||||
/** Comparison operators for numeric table filters. */
|
||||
TableNumberCompareTypeEnum: typeof TableNumberCompareTypeEnum;
|
||||
/** Comparison operators for text table filters. */
|
||||
TableStringCompareTypeEnum: typeof TableStringCompareTypeEnum;
|
||||
/** Comparison operators for date table filters. */
|
||||
TableDateCompareTypeEnum: typeof TableDateCompareTypeEnum;
|
||||
}
|
||||
|
||||
export class FSheetsTableEnumMixin extends FEnum implements IFSheetsTableEnumMixin {
|
||||
override get TableColumnFilterTypeEnum(): typeof TableColumnFilterTypeEnum {
|
||||
return TableColumnFilterTypeEnum;
|
||||
};
|
||||
|
||||
override get TableConditionTypeEnum(): typeof TableConditionTypeEnum {
|
||||
return TableConditionTypeEnum;
|
||||
};
|
||||
|
||||
override get TableNumberCompareTypeEnum(): typeof TableNumberCompareTypeEnum {
|
||||
return TableNumberCompareTypeEnum;
|
||||
}
|
||||
|
||||
override get TableStringCompareTypeEnum(): typeof TableStringCompareTypeEnum {
|
||||
return TableStringCompareTypeEnum;
|
||||
}
|
||||
|
||||
override get TableDateCompareTypeEnum(): typeof TableDateCompareTypeEnum {
|
||||
return TableDateCompareTypeEnum;
|
||||
}
|
||||
}
|
||||
|
||||
FEnum.extend(FSheetsTableEnumMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
interface FEnum extends IFSheetsTableEnumMixin { }
|
||||
}
|
||||
291
packages/sheets-table/src/facade/f-workbook.ts
Normal file
291
packages/sheets-table/src/facade/f-workbook.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type {
|
||||
IAddSheetTableCommandParams,
|
||||
IDeleteSheetTableParams,
|
||||
ISetSheetTableParams,
|
||||
ITableFilterItem,
|
||||
ITableInfo,
|
||||
ITableInfoWithUnitId,
|
||||
ITableOptions,
|
||||
ITableRange,
|
||||
} from '@univerjs/sheets-table';
|
||||
import {
|
||||
customNameCharacterCheck,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddSheetTableCommand,
|
||||
DeleteSheetTableCommand,
|
||||
SetSheetTableFilterCommand,
|
||||
SheetTableService,
|
||||
} from '@univerjs/sheets-table';
|
||||
import { FWorkbook } from '@univerjs/sheets/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorkbookSheetsTableMixin {
|
||||
/**
|
||||
* Get table information
|
||||
* @param {string} tableId The table id
|
||||
* @returns {ITableInfoWithUnitId | undefined} The table information, including workbook and worksheet IDs, or `undefined` if not found.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getTableInfo(tableId: string): ITableInfoWithUnitId | undefined;
|
||||
|
||||
/**
|
||||
* Get table information by name
|
||||
* @param {string} tableName The table name
|
||||
* @returns {ITableInfoWithUnitId | undefined} The table information, including workbook and worksheet IDs, or `undefined` if not found.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* const tableInfo = fWorkbook.getTableInfoByName('name-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getTableInfoByName(tableName: string): ITableInfoWithUnitId | undefined;
|
||||
|
||||
/**
|
||||
* Get table list
|
||||
* @returns {ITableInfo[]} The table list
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const tables = fWorkbook.getTableList();
|
||||
* console.log('debugger tables', tables);
|
||||
* ```
|
||||
*/
|
||||
getTableList(): ITableInfo[];
|
||||
|
||||
/**
|
||||
* Add table
|
||||
* @param {string} subUnitId The sub unit id
|
||||
* @param {string} tableName The table name
|
||||
* @param {ITableRange} rangeInfo The table range information
|
||||
* @param {string} [tableId] The table id
|
||||
* @param {ITableOptions} [options] The table options
|
||||
* @returns {Promise<string | undefined>} A promise resolving to the table ID, or `undefined` if the name is invalid or creation fails.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const id = await fWorkbook.addTable(
|
||||
* fWorksheet.getSheetId(),
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (id) {
|
||||
* const tableInfo = fWorkbook.getTableInfo(id);
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
addTable(subUnitId: string, tableName: string, rangeInfo: ITableRange, tableId?: string, options?: ITableOptions): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* set table filter
|
||||
* @param {string} tableId The table id
|
||||
* @param {number} column The column index, starting from 0.
|
||||
* @param {ITableFilterItem | undefined} filter The filter to apply, or `undefined` to clear the column filter.
|
||||
* @returns {Promise<boolean>} The result of set table filter
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* // Set the filter for the second column
|
||||
* await fWorkbook.setTableFilter('id-1', 1, {
|
||||
* filterType: univerAPI.Enum.TableColumnFilterTypeEnum.condition,
|
||||
* filterInfo: {
|
||||
* conditionType: univerAPI.Enum.TableConditionTypeEnum.Number,
|
||||
* compareType: univerAPI.Enum.TableNumberCompareTypeEnum.GreaterThan,
|
||||
* expectedValue: 10,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setTableFilter(tableId: string, column: number, filter: ITableFilterItem | undefined): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Remove table
|
||||
* @param {string} tableId The table id
|
||||
* @returns {Promise<boolean>} A promise resolving to whether the table was removed; `false` if the table is not found.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
*
|
||||
* if (tableInfo) {
|
||||
* // Remove the table with the specified id
|
||||
* await fWorkbook.removeTable('id-1');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
removeTable(tableId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class FWorkbookSheetsTableMixin extends FWorkbook implements IFWorkbookSheetsTableMixin {
|
||||
override getTableInfo(tableId: string): ITableInfoWithUnitId | undefined {
|
||||
const unitId = this.getId();
|
||||
const sheetTableService = this._injector.get(SheetTableService);
|
||||
return sheetTableService.getTableInfo(unitId, tableId);
|
||||
}
|
||||
|
||||
override getTableList(): ITableInfoWithUnitId[] {
|
||||
const unitId = this.getId();
|
||||
const sheetTableService = this._injector.get(SheetTableService);
|
||||
return sheetTableService.getTableList(unitId);
|
||||
}
|
||||
|
||||
override async addTable(
|
||||
subUnitId: string,
|
||||
tableName: string,
|
||||
rangeInfo: ITableRange,
|
||||
tableId?: string,
|
||||
options?: ITableOptions
|
||||
): Promise<string | undefined> {
|
||||
const sheetTableService = this._injector.get(SheetTableService);
|
||||
|
||||
const univerInstanceService = this._injector.get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const sheetNameSet = new Set<string>();
|
||||
if (workbook) {
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
sheetNameSet.add(sheet.getName());
|
||||
});
|
||||
}
|
||||
const isValidName = customNameCharacterCheck(tableName, sheetNameSet);
|
||||
if (!isValidName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const addTableParams: IAddSheetTableCommandParams = {
|
||||
unitId: this.getId(),
|
||||
name: tableName,
|
||||
subUnitId,
|
||||
range: rangeInfo,
|
||||
options,
|
||||
id: tableId,
|
||||
};
|
||||
const rs = await this._commandService.executeCommand(AddSheetTableCommand.id, addTableParams);
|
||||
if (rs) {
|
||||
return sheetTableService.getTableList(this.getId()).find((table) => table.name === tableName)?.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override async removeTable(tableId: string): Promise<boolean> {
|
||||
const subUnitId = this.getTableInfo(tableId)?.subUnitId;
|
||||
if (!subUnitId) {
|
||||
return false;
|
||||
}
|
||||
const removedTableParams: IDeleteSheetTableParams = {
|
||||
unitId: this.getId(),
|
||||
subUnitId,
|
||||
tableId,
|
||||
};
|
||||
return this._commandService.executeCommand(DeleteSheetTableCommand.id, removedTableParams);
|
||||
}
|
||||
|
||||
override getTableInfoByName(tableName: string): ITableInfoWithUnitId | undefined {
|
||||
const tableList = this.getTableList();
|
||||
return tableList.find((table) => table.name === tableName);
|
||||
}
|
||||
|
||||
override setTableFilter(tableId: string, column: number, filter: ITableFilterItem | undefined): Promise<boolean> {
|
||||
const setTableFilterParams: ISetSheetTableParams = {
|
||||
unitId: this.getId(),
|
||||
tableId,
|
||||
column,
|
||||
tableFilter: filter,
|
||||
};
|
||||
return this._commandService.executeCommand(SetSheetTableFilterCommand.id, setTableFilterParams);
|
||||
}
|
||||
}
|
||||
|
||||
FWorkbook.extend(FWorkbookSheetsTableMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FWorkbook extends IFWorkbookSheetsTableMixin { }
|
||||
}
|
||||
478
packages/sheets-table/src/facade/f-worksheet.ts
Normal file
478
packages/sheets-table/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* 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 { IRangeThemeStyleJSON } from '@univerjs/sheets';
|
||||
import type {
|
||||
IAddSheetTableCommandParams,
|
||||
IDeleteSheetTableParams,
|
||||
ISetSheetTableCommandParams,
|
||||
ISetSheetTableParams,
|
||||
ITableFilterItem,
|
||||
ITableInfoWithUnitId,
|
||||
ITableOptions,
|
||||
ITableRange,
|
||||
} from '@univerjs/sheets-table';
|
||||
import { cellToRange, customNameCharacterCheck, LocaleService, Rectangle } from '@univerjs/core';
|
||||
import { RangeThemeStyle } from '@univerjs/sheets';
|
||||
import {
|
||||
AddSheetTableCommand,
|
||||
AddTableThemeCommand,
|
||||
DeleteSheetTableCommand,
|
||||
SetSheetTableCommand,
|
||||
SetSheetTableFilterCommand,
|
||||
SheetTableService,
|
||||
} from '@univerjs/sheets-table';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetTableMixin {
|
||||
/**
|
||||
* Add a table to the worksheet
|
||||
* @param {string} tableName The table name
|
||||
* @param {ITableRange} rangeInfo The table range information
|
||||
* @param {string} [tableId] The table id
|
||||
* @param {ITableOptions} [options] The table options
|
||||
* @returns {Promise<boolean> | boolean} `false` for an invalid table name; otherwise, a promise resolving to whether the command succeeded.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* columns: [
|
||||
* { id: 'col-1', displayName: 'Column 1' }
|
||||
* ],
|
||||
* filters: [
|
||||
* {
|
||||
* filterType: univerAPI.Enum.TableColumnFilterTypeEnum.condition,
|
||||
* filterInfo: {
|
||||
* conditionType: univerAPI.Enum.TableConditionTypeEnum.Number,
|
||||
* compareType: univerAPI.Enum.TableNumberCompareTypeEnum.GreaterThan,
|
||||
* expectedValue: 2,
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
addTable(tableName: string, rangeInfo: ITableRange, tableId?: string, options?: ITableOptions): Promise<boolean> | boolean;
|
||||
|
||||
/**
|
||||
* Set the filter for a table column
|
||||
* @param {string} tableId The table id
|
||||
* @param {number} column The table column index, starting from 0. For example, the first column is 0, the second column is 1, and so on.
|
||||
* @param {ITableFilterItem} filter The filter item
|
||||
* @returns {Promise<boolean>} Whether the table filter was set successfully
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* // Set the filter for the second column
|
||||
* await fWorksheet.setTableFilter('id-1', 1, {
|
||||
* filterType: univerAPI.Enum.TableColumnFilterTypeEnum.condition,
|
||||
* filterInfo: {
|
||||
* conditionType: univerAPI.Enum.TableConditionTypeEnum.Number,
|
||||
* compareType: univerAPI.Enum.TableNumberCompareTypeEnum.GreaterThan,
|
||||
* expectedValue: 10,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setTableFilter(tableId: string, column: number, filter: ITableFilterItem): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Remove a table from the worksheet
|
||||
* @param {string} tableId The table id
|
||||
* @returns {Promise<boolean>} Whether the table was removed successfully
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
*
|
||||
* if (tableInfo) {
|
||||
* // Remove the table with the specified id
|
||||
* await fWorksheet.removeTable('id-1');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
removeTable(tableId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Set the range of a table
|
||||
* @param {string} tableId The table id
|
||||
* @param {ITableRange} rangeInfo The new range information
|
||||
* @returns {Promise<boolean>} Whether the table range was set successfully
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* // Update the table range to B2:F21 after 3 seconds
|
||||
* setTimeout(async () => {
|
||||
* const newRange = fWorksheet.getRange('B2:F21');
|
||||
* await fWorksheet.setTableRange('id-1', newRange.getRange());
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }, 3000);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setTableRange(tableId: string, rangeInfo: ITableRange): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Set the name of a table
|
||||
* @param {string} tableId The table id
|
||||
* @param {string} tableName The new table name
|
||||
* @returns {Promise<boolean> | boolean} `false` for an invalid table name; otherwise, a promise resolving to whether the command succeeded.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* // Update the table name after 3 seconds
|
||||
* setTimeout(async () => {
|
||||
* await fWorksheet.setTableName('id-1', 'new-name');
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }, 3000);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setTableName(tableId: string, tableName: string): Promise<boolean> | boolean;
|
||||
|
||||
/**
|
||||
* Get the list of tables in the worksheet
|
||||
* @returns {ITableInfoWithUnitId[]} The list of tables
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const tables = fWorksheet.getSubTableInfos();
|
||||
* console.log('debugger tables', tables);
|
||||
* ```
|
||||
*/
|
||||
getSubTableInfos(): ITableInfoWithUnitId[];
|
||||
|
||||
/**
|
||||
* Reset the column filter of a table
|
||||
* @param {string} tableId The table id
|
||||
* @param {number} column The column index, starting from 0. For example, the first column is 0, the second column is 1, and so on.
|
||||
* @returns {Promise<boolean>} Whether the table filter was reset successfully
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* // Set the filter for the second column
|
||||
* await fWorksheet.setTableFilter('id-1', 1, {
|
||||
* filterType: univerAPI.Enum.TableColumnFilterTypeEnum.condition,
|
||||
* filterInfo: {
|
||||
* conditionType: univerAPI.Enum.TableConditionTypeEnum.Number,
|
||||
* compareType: univerAPI.Enum.TableNumberCompareTypeEnum.GreaterThan,
|
||||
* expectedValue: 10,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Reset the filter for the second column after 3 seconds
|
||||
* setTimeout(async () => {
|
||||
* await fWorksheet.resetFilter('id-1', 1);
|
||||
* }, 3000);
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
resetFilter(tableId: string, column: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the table information by cell position
|
||||
* @param {number} row The cell row index, starting from 0.
|
||||
* @param {number} column The cell column index, starting from 0.
|
||||
* @returns {ITableInfoWithUnitId | undefined} The table information or undefined if not found
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* const cellB2 = fWorksheet.getRange('B2');
|
||||
* const row = cellB2.getRow();
|
||||
* const column = cellB2.getColumn();
|
||||
* console.log('debugger tableInfo', fWorksheet.getTableByCell(row, column));
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
* console.log('debugger tableInfo2', fWorksheet.getTableByCell(row, column));
|
||||
* ```
|
||||
*/
|
||||
getTableByCell(row: number, column: number): ITableInfoWithUnitId | undefined;
|
||||
|
||||
/**
|
||||
* Add a theme to the table
|
||||
* @param {string} tableId The table id
|
||||
* @param {IRangeThemeStyleJSON} themeStyleJSON The theme style JSON
|
||||
* @returns {Promise<boolean>} Whether the theme was added successfully
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert a table in the range B2:F11
|
||||
* const fRange = fWorksheet.getRange('B2:F11');
|
||||
* const success = await fWorksheet.addTable(
|
||||
* 'name-1',
|
||||
* fRange.getRange(),
|
||||
* 'id-1',
|
||||
* {
|
||||
* tableStyleId: 'table-default-4',
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* if (success) {
|
||||
* await fWorksheet.addTableTheme('id-1', {
|
||||
* name: 'table-custom-1',
|
||||
* headerRowStyle: {
|
||||
* bg: {
|
||||
* rgb: '#145f82',
|
||||
* },
|
||||
* },
|
||||
* firstRowStyle: {
|
||||
* bg: {
|
||||
* rgb: '#c0e4f5',
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* const tableInfo = fWorkbook.getTableInfo('id-1');
|
||||
* console.log('debugger tableInfo', tableInfo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
addTableTheme(tableId: string, themeStyleJSON: IRangeThemeStyleJSON): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class FWorksheetTableMixin extends FWorksheet implements IFWorksheetTableMixin {
|
||||
override addTable(tableName: string, rangeInfo: ITableRange, tableId?: string, options?: ITableOptions): Promise<boolean> | boolean {
|
||||
const subUnitId = this.getSheetId();
|
||||
const workbook = this.getWorkbook();
|
||||
const unitId = workbook.getUnitId();
|
||||
|
||||
const localeService = this._injector.get(LocaleService);
|
||||
|
||||
const sheetNameSet = new Set<string>();
|
||||
if (workbook) {
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
sheetNameSet.add(sheet.getName());
|
||||
});
|
||||
}
|
||||
const isValidName = customNameCharacterCheck(tableName, sheetNameSet);
|
||||
if (!isValidName) {
|
||||
return false;
|
||||
}
|
||||
const addTableParams: IAddSheetTableCommandParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
name: tableName,
|
||||
range: rangeInfo,
|
||||
id: tableId,
|
||||
options,
|
||||
};
|
||||
return this._commandService.executeCommand(AddSheetTableCommand.id, addTableParams);
|
||||
}
|
||||
|
||||
override setTableFilter(tableId: string, column: number, filter: ITableFilterItem): Promise<boolean> {
|
||||
const setTableFilterParams: ISetSheetTableParams = {
|
||||
unitId: this.getWorkbook().getUnitId(),
|
||||
tableId,
|
||||
column,
|
||||
tableFilter: filter,
|
||||
};
|
||||
return this._commandService.executeCommand(SetSheetTableFilterCommand.id, setTableFilterParams);
|
||||
}
|
||||
|
||||
override removeTable(tableId: string): Promise<boolean> {
|
||||
const removedTableParams: IDeleteSheetTableParams = {
|
||||
unitId: this._fWorkbook.getId(),
|
||||
subUnitId: this.getSheetId(),
|
||||
tableId,
|
||||
};
|
||||
return this._commandService.executeCommand(DeleteSheetTableCommand.id, removedTableParams);
|
||||
}
|
||||
|
||||
override setTableRange(tableId: string, rangeInfo: ITableRange): Promise<boolean> {
|
||||
const tableSetConfig: ISetSheetTableCommandParams = {
|
||||
unitId: this.getWorkbook().getUnitId(),
|
||||
tableId,
|
||||
updateRange: {
|
||||
newRange: rangeInfo,
|
||||
},
|
||||
};
|
||||
return this._commandService.executeCommand(SetSheetTableCommand.id, tableSetConfig);
|
||||
}
|
||||
|
||||
override setTableName(tableId: string, tableName: string): Promise<boolean> | boolean {
|
||||
const workbook = this.getWorkbook();
|
||||
|
||||
const sheetNameSet = new Set<string>();
|
||||
if (workbook) {
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
sheetNameSet.add(sheet.getName());
|
||||
});
|
||||
}
|
||||
const isValidName = customNameCharacterCheck(tableName, sheetNameSet);
|
||||
if (!isValidName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tableSetConfig: ISetSheetTableCommandParams = {
|
||||
unitId: this.getWorkbook().getUnitId(),
|
||||
tableId,
|
||||
name: tableName,
|
||||
};
|
||||
return this._commandService.executeCommand(SetSheetTableCommand.id, tableSetConfig);
|
||||
}
|
||||
|
||||
override getSubTableInfos(): ITableInfoWithUnitId[] {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const sheetTableService = this._injector.get(SheetTableService);
|
||||
return sheetTableService.getTableList(unitId).filter((table) => table.subUnitId === this.getSheetId());
|
||||
}
|
||||
|
||||
override resetFilter(tableId: string, column: number): Promise<boolean> {
|
||||
const setTableFilterParams: ISetSheetTableParams = {
|
||||
unitId: this._fWorkbook.getId(),
|
||||
tableId,
|
||||
column,
|
||||
tableFilter: undefined,
|
||||
};
|
||||
return this._commandService.executeCommand(SetSheetTableFilterCommand.id, setTableFilterParams);
|
||||
}
|
||||
|
||||
override getTableByCell(row: number, column: number): ITableInfoWithUnitId | undefined {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const sheetTableService = this._injector.get(SheetTableService);
|
||||
const allSubTableInfos = sheetTableService.getTableList(unitId).filter((table) => table.subUnitId === this.getSheetId());
|
||||
const cellRange = cellToRange(row, column);
|
||||
return allSubTableInfos.find((table) => {
|
||||
const tableRange = table.range;
|
||||
return Rectangle.intersects(tableRange, cellRange);
|
||||
});
|
||||
}
|
||||
|
||||
override addTableTheme(tableId: string, themeStyleJSON: IRangeThemeStyleJSON): Promise<boolean> {
|
||||
const themeStyle = new RangeThemeStyle('table-style');
|
||||
themeStyle.fromJson(themeStyleJSON);
|
||||
return this._commandService.executeCommand(AddTableThemeCommand.id, {
|
||||
unitId: this._fWorkbook.getId(),
|
||||
tableId,
|
||||
themeStyle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetTableMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FWorksheet extends IFWorksheetTableMixin { }
|
||||
}
|
||||
22
packages/sheets-table/src/facade/index.ts
Normal file
22
packages/sheets-table/src/facade/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 './f-workbook';
|
||||
import './f-worksheet';
|
||||
import './f-enum';
|
||||
|
||||
export type * from './f-workbook';
|
||||
export type * from './f-worksheet';
|
||||
80
packages/sheets-table/src/index.ts
Normal file
80
packages/sheets-table/src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 { AddSheetTableCommand } from './commands/commands/add-sheet-table.command';
|
||||
export type { IAddSheetTableCommandParams } from './commands/commands/add-sheet-table.command';
|
||||
export type { IAddTableThemeCommandParams } from './commands/commands/add-table-theme.command';
|
||||
export { AddTableThemeCommand } from './commands/commands/add-table-theme.command';
|
||||
export { DeleteSheetTableCommand } from './commands/commands/delete-sheet-table.command';
|
||||
export { RemoveTableThemeCommand } from './commands/commands/remove-table-theme.command';
|
||||
export { SetSheetTableCommand } from './commands/commands/set-sheet-table.command';
|
||||
export type { ISetSheetTableCommandParams } from './commands/commands/set-sheet-table.command';
|
||||
export { SetSheetTableFilterCommand } from './commands/commands/set-table-filter.command';
|
||||
export { SetSheetTableSortStateCommand } from './commands/commands/set-table-sort-state.command';
|
||||
export {
|
||||
SheetTableInsertColCommand,
|
||||
SheetTableInsertColumnAtCommand,
|
||||
SheetTableInsertRowAtCommand,
|
||||
SheetTableInsertRowCommand,
|
||||
SheetTableRemoveColCommand,
|
||||
SheetTableRemoveColumnAtCommand,
|
||||
SheetTableRemoveRowCommand,
|
||||
} from './commands/commands/sheet-table-row-col.command';
|
||||
export { AddSheetTableMutation } from './commands/mutations/add-sheet-table.mutation';
|
||||
export type { IAddSheetTableParams } from './commands/mutations/add-sheet-table.mutation';
|
||||
export { DeleteSheetTableMutation } from './commands/mutations/delete-sheet-table.mutation';
|
||||
export type { IDeleteSheetTableParams } from './commands/mutations/delete-sheet-table.mutation';
|
||||
export { SetSheetTableMutation } from './commands/mutations/set-sheet-table.mutation';
|
||||
export type { ISetSheetTableMutationParams } from './commands/mutations/set-sheet-table.mutation';
|
||||
export { SetSheetTableFilterMutation } from './commands/mutations/set-table-filter.mutation';
|
||||
export type { ISetSheetTableParams } from './commands/mutations/set-table-filter.mutation';
|
||||
export type { IUniverSheetsTableConfig } from './config/config';
|
||||
export { SHEET_TABLE_CUSTOM_THEME_PREFIX, TABLE_FILTER_EMPTY_VALUE } from './const';
|
||||
export { SheetsTableThemeController } from './controllers/sheet-table-theme.controller';
|
||||
export { SheetsTableController } from './controllers/sheets-table.controller';
|
||||
export { customEmptyThemeWithBorderStyle, processStyleWithBorderStyle } from './controllers/table-theme.factory';
|
||||
export { TableManager } from './models/table-manager';
|
||||
export { UniverSheetsTablePlugin } from './plugin';
|
||||
export { SheetTableService } from './services/table.service';
|
||||
export {
|
||||
SheetsTableButtonStateEnum,
|
||||
SheetsTableSortStateEnum,
|
||||
TableColumnDataTypeEnum,
|
||||
TableColumnFilterTypeEnum,
|
||||
TableConditionTypeEnum,
|
||||
TableDateCompareTypeEnum,
|
||||
TableNumberCompareTypeEnum,
|
||||
TableStringCompareTypeEnum,
|
||||
} from './types/enum';
|
||||
export type {
|
||||
ITableColorFilterItem,
|
||||
ITableColumnJson,
|
||||
ITableConditionFilterItem,
|
||||
ITableData,
|
||||
ITableFilterItem,
|
||||
ITableInfo,
|
||||
ITableInfoWithUnitId,
|
||||
ITableManualFilterItem,
|
||||
ITableOptions,
|
||||
ITableRange,
|
||||
ITableRangeWithState,
|
||||
TableMetaType,
|
||||
TableRelationTupleType,
|
||||
} from './types/type';
|
||||
export type { ITableJson, ITableSetConfig } from './types/type';
|
||||
export { getExistingNamesSet, isColorTableFilter, isConditionFilter, isManualTableFilter } from './util';
|
||||
export { validateSheetTableName } from './util/table-name';
|
||||
export type { ISheetTableNameValidationResult, SheetTableNameValidationReason } from './util/table-name';
|
||||
27
packages/sheets-table/src/locale/ar-SA.ts
Normal file
27
packages/sheets-table/src/locale/ar-SA.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'عمود',
|
||||
tablePrefix: 'جدول',
|
||||
tableNameError: 'لا يمكن أن يحتوي اسم الجدول على مسافات، ولا يمكن أن يبدأ برقم، ولا يمكن أن يكون مطابقًا لاسم جدول موجود',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/ca-ES.ts
Normal file
27
packages/sheets-table/src/locale/ca-ES.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Columna',
|
||||
tablePrefix: 'Taula',
|
||||
tableNameError: 'El nom de la taula no pot contenir espais, no pot començar amb un número i no pot ser idèntic a un nom de taula existent',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/de-DE.ts
Normal file
27
packages/sheets-table/src/locale/de-DE.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Spalte',
|
||||
tablePrefix: 'Tabelle',
|
||||
tableNameError: 'Tabellenname darf keine Leerzeichen enthalten, darf nicht mit einer Zahl beginnen und darf nicht mit einem vorhandenen Tabellennamen identisch sein',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
25
packages/sheets-table/src/locale/en-US.ts
Normal file
25
packages/sheets-table/src/locale/en-US.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Column',
|
||||
tablePrefix: 'Table',
|
||||
tableNameError: 'Table name cannot contain spaces, cannot start with a number, and cannot be identical to an existing table name',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/es-ES.ts
Normal file
27
packages/sheets-table/src/locale/es-ES.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Columna',
|
||||
tablePrefix: 'Tabla',
|
||||
tableNameError: 'El nombre de la tabla no puede contener espacios, no puede comenzar con un número y no puede ser idéntico a un nombre de tabla existente',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/fa-IR.ts
Normal file
27
packages/sheets-table/src/locale/fa-IR.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'ستون',
|
||||
tablePrefix: 'جدول',
|
||||
tableNameError: 'نام جدول نمیتواند شامل فاصله باشد، نمیتواند با عدد شروع شود و نمیتواند با نام جدولهای موجود تکراری باشد',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/fr-FR.ts
Normal file
27
packages/sheets-table/src/locale/fr-FR.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Colonne',
|
||||
tablePrefix: 'Tableau',
|
||||
tableNameError: 'Le nom du tableau ne peut pas contenir d\'espaces, ne peut pas commencer par un chiffre et ne peut pas être identique à un nom de tableau existant',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/id-ID.ts
Normal file
27
packages/sheets-table/src/locale/id-ID.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Kolom',
|
||||
tablePrefix: 'Tabel',
|
||||
tableNameError: 'Nama tabel tidak boleh mengandung spasi, tidak boleh diawali dengan angka, dan tidak boleh sama dengan nama tabel yang sudah ada',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/it-IT.ts
Normal file
27
packages/sheets-table/src/locale/it-IT.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Colonna',
|
||||
tablePrefix: 'Tabella',
|
||||
tableNameError: 'Il nome della tabella non può contenere spazi, non può iniziare con un numero e non può essere identico a un nome di tabella esistente',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/ja-JP.ts
Normal file
27
packages/sheets-table/src/locale/ja-JP.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: '列',
|
||||
tablePrefix: '表',
|
||||
tableNameError: '表の名前は空白を含めることはできず、数字で始めることはできず、既存の表名と重複できません',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/ko-KR.ts
Normal file
27
packages/sheets-table/src/locale/ko-KR.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: '열',
|
||||
tablePrefix: '표',
|
||||
tableNameError: '표 이름은 공백을 포함할 수 없으며 숫자로 시작할 수 없고 기존 표 이름과 중복될 수 없습니다',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/pl-PL.ts
Normal file
27
packages/sheets-table/src/locale/pl-PL.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Kolumna',
|
||||
tablePrefix: 'Tabela',
|
||||
tableNameError: 'Nazwa tabeli nie może zawierać spacji, nie może zaczynać się od cyfry i nie może być identyczna z istniejącą nazwą tabeli',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/pt-BR.ts
Normal file
27
packages/sheets-table/src/locale/pt-BR.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Coluna',
|
||||
tablePrefix: 'Tabela',
|
||||
tableNameError: 'O nome da tabela não pode conter espaços, não pode começar com um número e não pode ser idêntico a um nome de tabela existente',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/ru-RU.ts
Normal file
27
packages/sheets-table/src/locale/ru-RU.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Столбец',
|
||||
tablePrefix: 'Таблица',
|
||||
tableNameError: 'Имя таблицы не может содержать пробелы, не может начинаться с цифры и не может совпадать с именем существующей таблицы',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/sk-SK.ts
Normal file
27
packages/sheets-table/src/locale/sk-SK.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Stĺpec',
|
||||
tablePrefix: 'Tabuľka',
|
||||
tableNameError: 'Názov tabuľky nesmie obsahovať medzery, nesmie začínať číslom a nesmie byť zhodný s existujúcim názvom tabuľky',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-table/src/locale/types.ts
Normal file
20
packages/sheets-table/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
27
packages/sheets-table/src/locale/vi-VN.ts
Normal file
27
packages/sheets-table/src/locale/vi-VN.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: 'Cột',
|
||||
tablePrefix: 'Bảng',
|
||||
tableNameError: 'Tên bảng không thể chứa khoảng trắng, không được bắt đầu bằng số và không được trùng với tên bảng đã có',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/zh-CN.ts
Normal file
27
packages/sheets-table/src/locale/zh-CN.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: '列',
|
||||
tablePrefix: '表格',
|
||||
tableNameError: '表格名称不能包含空格, 不能以数字开头,不能和已有表格名称重复',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/zh-HK.ts
Normal file
27
packages/sheets-table/src/locale/zh-HK.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: '列',
|
||||
tablePrefix: '表格',
|
||||
tableNameError: '表格名稱不能包含空格, 不能以數字開頭,不能和已有表格名稱重複',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/sheets-table/src/locale/zh-TW.ts
Normal file
27
packages/sheets-table/src/locale/zh-TW.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.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-table': {
|
||||
columnPrefix: '列',
|
||||
tablePrefix: '表格',
|
||||
tableNameError: '表格名稱不能包含空格, 不能以數字開頭,不能和已有表格名稱重複',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -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 { IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import { LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { TableManager } from '../table-manager';
|
||||
|
||||
describe('TableManager', () => {
|
||||
let univer: Univer | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
univer?.dispose();
|
||||
});
|
||||
|
||||
it('preserves the table style id in add events emitted during deserialization', () => {
|
||||
univer = new Univer();
|
||||
univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
{
|
||||
id: 'unit-1',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'table test',
|
||||
sheetOrder: ['sheet-1'],
|
||||
sheets: {
|
||||
'sheet-1': {
|
||||
id: 'sheet-1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 2,
|
||||
columnCount: 1,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'Name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
}
|
||||
);
|
||||
|
||||
const manager = univer.__getInjector().createInstance(TableManager);
|
||||
const events: Array<{ tableStyleId?: string }> = [];
|
||||
manager.tableAdd$.subscribe((event) => events.push(event));
|
||||
|
||||
manager.fromJSON('unit-1', {
|
||||
'sheet-1': {
|
||||
tables: [
|
||||
{
|
||||
id: 'table-1',
|
||||
name: 'Table1',
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 0,
|
||||
},
|
||||
options: {
|
||||
tableStyleId: 'ImportedTableStyle',
|
||||
},
|
||||
filters: {},
|
||||
columns: [],
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
tableFilteredOutRows: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].tableStyleId).toBe('ImportedTableStyle');
|
||||
});
|
||||
});
|
||||
332
packages/sheets-table/src/models/__tests__/table-model.spec.ts
Normal file
332
packages/sheets-table/src/models/__tests__/table-model.spec.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 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 { ITableColorFilterItem } from '../../types/type';
|
||||
import { CellValueType, ThemeColorType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TABLE_FILTER_EMPTY_VALUE } from '../../const';
|
||||
import {
|
||||
SheetsTableButtonStateEnum,
|
||||
SheetsTableSortStateEnum,
|
||||
TableColumnDataTypeEnum,
|
||||
TableColumnFilterTypeEnum,
|
||||
TableConditionTypeEnum,
|
||||
TableNumberCompareTypeEnum,
|
||||
} from '../../types/enum';
|
||||
import { Table } from '../table';
|
||||
import { TableColumn } from '../table-column';
|
||||
import { TableFilters } from '../table-filter';
|
||||
|
||||
describe('Table model', () => {
|
||||
it('builds a table from headers, tracks metadata, and serializes user changes', () => {
|
||||
const table = new Table(
|
||||
'table-1',
|
||||
'QuarterlySales',
|
||||
{ startRow: 0, endRow: 4, startColumn: 0, endColumn: 1 },
|
||||
['Region', 'Amount'],
|
||||
{
|
||||
showHeader: true,
|
||||
tableStyleId: 'theme-2',
|
||||
columns: [
|
||||
{ id: 'column-region', displayName: 'Region', dataType: TableColumnDataTypeEnum.String, formula: '', meta: {}, style: {} },
|
||||
{ id: 'column-amount', displayName: 'Amount', dataType: TableColumnDataTypeEnum.String, formula: '', meta: {}, style: {} },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
table.setSubunitId('sheet-1');
|
||||
table.setTableMeta({ owner: 'ops' });
|
||||
table.insertColumn(2, new TableColumn('column-margin', 'Margin'));
|
||||
table.removeColumn(2);
|
||||
table.setDisplayName('FY Sales');
|
||||
table.setRange({ startRow: 1, endRow: 6, startColumn: 0, endColumn: 1 });
|
||||
|
||||
expect(table.getTableFilterRange()).toEqual({
|
||||
startRow: 2,
|
||||
endRow: 6,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
});
|
||||
expect(table.getColumnsCount()).toBe(2);
|
||||
expect(table.getColumnNameByIndex(1)).toBe('Amount');
|
||||
expect(table.getTableInfo()).toEqual(expect.objectContaining({
|
||||
id: 'table-1',
|
||||
subUnitId: 'sheet-1',
|
||||
name: 'FY Sales',
|
||||
range: { startRow: 1, endRow: 6, startColumn: 0, endColumn: 1 },
|
||||
meta: { owner: 'ops' },
|
||||
showHeader: true,
|
||||
columns: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'column-region', displayName: 'Region' }),
|
||||
expect.objectContaining({ id: 'column-amount', displayName: 'Amount' }),
|
||||
]),
|
||||
}));
|
||||
expect(table.getTableConfig()).toEqual({
|
||||
name: 'FY Sales',
|
||||
range: { startRow: 1, endRow: 6, startColumn: 0, endColumn: 1 },
|
||||
options: {
|
||||
showHeader: true,
|
||||
showFooter: false,
|
||||
},
|
||||
tableStyleId: 'theme-2',
|
||||
});
|
||||
|
||||
const snapshot = table.toJSON();
|
||||
const restored = new Table('placeholder', 'Placeholder', { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, []);
|
||||
restored.fromJSON(snapshot);
|
||||
|
||||
expect(restored.getDisplayName()).toBe('FY Sales');
|
||||
expect(restored.getRangeInfo()).toEqual({ startRow: 1, endRow: 6, startColumn: 0, endColumn: 1 });
|
||||
expect(restored.getTableMeta()).toEqual({ owner: 'ops' });
|
||||
expect(restored.toJSON().columns).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'column-region', displayName: 'Region' }),
|
||||
expect.objectContaining({ id: 'column-amount', displayName: 'Amount' }),
|
||||
]));
|
||||
|
||||
restored.dispose();
|
||||
expect(restored.getColumnsCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TableFilters', () => {
|
||||
it('filters rows by manual lists and condition rules like a real table filter popup', () => {
|
||||
const filters = new TableFilters();
|
||||
const sheet = {
|
||||
getCell(row: number, column: number) {
|
||||
const cells = [
|
||||
[{ v: 'East' }, { v: 12, t: CellValueType.NUMBER }],
|
||||
[{ v: 'West' }, { v: 5, t: CellValueType.NUMBER }],
|
||||
[{ v: 'East' }, { v: 20, t: CellValueType.NUMBER }],
|
||||
[null, { v: 30, t: CellValueType.NUMBER }],
|
||||
];
|
||||
|
||||
return cells[row]?.[column] ?? null;
|
||||
},
|
||||
getCellRaw(row: number, column: number) {
|
||||
return this.getCell(row, column);
|
||||
},
|
||||
};
|
||||
const range = { startRow: 0, endRow: 3, startColumn: 0, endColumn: 1 };
|
||||
|
||||
filters.setColumnFilter(0, {
|
||||
filterType: TableColumnFilterTypeEnum.manual,
|
||||
values: ['East', TABLE_FILTER_EMPTY_VALUE],
|
||||
} as never);
|
||||
filters.setColumnFilter(1, {
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.GreaterThan,
|
||||
expectedValue: [10],
|
||||
},
|
||||
} as never);
|
||||
filters.setSortState(0, SheetsTableSortStateEnum.Asc);
|
||||
|
||||
const filteredOutRows = filters.doFilter(sheet as never, range);
|
||||
|
||||
expect([...filteredOutRows]).toEqual([1]);
|
||||
expect(filters.getColumnFilter(0)).toEqual({
|
||||
filterType: TableColumnFilterTypeEnum.manual,
|
||||
values: ['East', TABLE_FILTER_EMPTY_VALUE],
|
||||
});
|
||||
expect(filters.getFilterState(0)).toBe(SheetsTableButtonStateEnum.FilteredSortAsc);
|
||||
expect(filters.getFilterStates(range)).toEqual([
|
||||
SheetsTableButtonStateEnum.FilteredSortAsc,
|
||||
SheetsTableButtonStateEnum.FilteredSortNone,
|
||||
]);
|
||||
expect(filters.getFilterOutRows()).toBe(filteredOutRows);
|
||||
expect(filters.toJSON()).toEqual({
|
||||
tableColumnFilterList: [
|
||||
{ filterType: TableColumnFilterTypeEnum.manual, values: ['East', TABLE_FILTER_EMPTY_VALUE] },
|
||||
{
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.GreaterThan,
|
||||
expectedValue: [10],
|
||||
},
|
||||
},
|
||||
],
|
||||
tableSortInfo: { columnIndex: 0, sortState: SheetsTableSortStateEnum.Asc },
|
||||
});
|
||||
|
||||
filters.dispose();
|
||||
expect(filters.toJSON().tableColumnFilterList).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters rows by cell fill colors', () => {
|
||||
const filters = new TableFilters();
|
||||
const cells = [
|
||||
{ v: 'Red', s: { bg: { rgb: '#ff0000' } } },
|
||||
{ v: 'Blue', s: { bg: { rgb: '#0000ff' } } },
|
||||
{ v: 'Default' },
|
||||
];
|
||||
const sheet = {
|
||||
getCell(row: number) {
|
||||
return cells[row] ?? null;
|
||||
},
|
||||
getCellRaw(row: number) {
|
||||
return this.getCell(row);
|
||||
},
|
||||
getComposedCellStyleByCellData(_row: number, _column: number, cell: typeof cells[number]) {
|
||||
return cell?.s ?? {};
|
||||
},
|
||||
};
|
||||
|
||||
filters.setColumnFilter(0, {
|
||||
filterType: TableColumnFilterTypeEnum.color,
|
||||
cellFillColors: ['rgb(255, 0, 0)'],
|
||||
});
|
||||
|
||||
expect([...filters.doFilter(sheet as never, {
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
})]).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('filters rows by theme fill colors', () => {
|
||||
const filters = new TableFilters();
|
||||
const cells = [
|
||||
{ v: 'Theme', s: { bg: { th: ThemeColorType.ACCENT1 } } },
|
||||
{ v: 'Red', s: { bg: { rgb: '#ff0000' } } },
|
||||
];
|
||||
const sheet = {
|
||||
getCell(row: number) {
|
||||
return cells[row] ?? null;
|
||||
},
|
||||
getCellRaw(row: number) {
|
||||
return this.getCell(row);
|
||||
},
|
||||
getComposedCellStyleByCellData(_row: number, _column: number, cell: typeof cells[number]) {
|
||||
return cell?.s ?? {};
|
||||
},
|
||||
};
|
||||
|
||||
filters.setColumnFilter(0, {
|
||||
filterType: TableColumnFilterTypeEnum.color,
|
||||
cellFillColors: ['rgb(68, 114, 196)'],
|
||||
});
|
||||
|
||||
expect([...filters.doFilter(sheet as never, {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
})]).toEqual([1]);
|
||||
});
|
||||
|
||||
it('filters rows by the default fill color', () => {
|
||||
const filters = new TableFilters();
|
||||
const cells = [
|
||||
{ v: 'Red', s: { bg: { rgb: '#ff0000' } } },
|
||||
{ v: 'Default' },
|
||||
];
|
||||
const sheet = {
|
||||
getCell(row: number) {
|
||||
return cells[row] ?? null;
|
||||
},
|
||||
getCellRaw(row: number) {
|
||||
return this.getCell(row);
|
||||
},
|
||||
getComposedCellStyleByCellData(_row: number, _column: number, cell: typeof cells[number]) {
|
||||
return cell?.s ?? {};
|
||||
},
|
||||
};
|
||||
|
||||
filters.setColumnFilter(0, {
|
||||
filterType: TableColumnFilterTypeEnum.color,
|
||||
cellFillColors: [null],
|
||||
});
|
||||
|
||||
expect([...filters.doFilter(sheet as never, {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
})]).toEqual([0]);
|
||||
});
|
||||
|
||||
it('filters rows by cell text colors and preserves the filter in JSON', () => {
|
||||
const filters = new TableFilters();
|
||||
const cells = [
|
||||
{ v: 'Red', s: { cl: { rgb: '#ff0000' } } },
|
||||
{ v: 'Blue', s: { cl: { rgb: '#0000ff' } } },
|
||||
{ v: 'Default' },
|
||||
];
|
||||
const sheet = {
|
||||
getCell(row: number) {
|
||||
return cells[row] ?? null;
|
||||
},
|
||||
getCellRaw(row: number) {
|
||||
return this.getCell(row);
|
||||
},
|
||||
getComposedCellStyleByCellData(_row: number, _column: number, cell: typeof cells[number]) {
|
||||
return cell?.s ?? {};
|
||||
},
|
||||
};
|
||||
const filter: ITableColorFilterItem = {
|
||||
filterType: TableColumnFilterTypeEnum.color,
|
||||
cellTextColors: ['rgb(0, 0, 255)'],
|
||||
};
|
||||
|
||||
filters.setColumnFilter(0, filter);
|
||||
|
||||
expect([...filters.doFilter(sheet as never, {
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
})]).toEqual([0, 2]);
|
||||
expect(filters.toJSON().tableColumnFilterList).toEqual([filter]);
|
||||
|
||||
const restored = new TableFilters();
|
||||
restored.fromJSON(filters.toJSON());
|
||||
expect(restored.getColumnFilter(0)).toEqual(filter);
|
||||
});
|
||||
|
||||
it('treats the default text color as black', () => {
|
||||
const filters = new TableFilters();
|
||||
const cells = [
|
||||
{ v: 'Blue', s: { cl: { rgb: '#0000ff' } } },
|
||||
{ v: 'Default', s: { cl: { rgb: null } } },
|
||||
];
|
||||
const sheet = {
|
||||
getCell(row: number) {
|
||||
return cells[row] ?? null;
|
||||
},
|
||||
getCellRaw(row: number) {
|
||||
return this.getCell(row);
|
||||
},
|
||||
getComposedCellStyleByCellData(_row: number, _column: number, cell: typeof cells[number]) {
|
||||
return cell?.s ?? {};
|
||||
},
|
||||
};
|
||||
|
||||
filters.setColumnFilter(0, {
|
||||
filterType: TableColumnFilterTypeEnum.color,
|
||||
cellTextColors: ['rgb(0, 0, 0)'],
|
||||
});
|
||||
|
||||
expect([...filters.doFilter(sheet as never, {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
})]).toEqual([0]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,513 @@
|
||||
/**
|
||||
* 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, Workbook } from '@univerjs/core';
|
||||
import { CellValueType, DateSystem, LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
TableColumnFilterTypeEnum,
|
||||
TableConditionTypeEnum,
|
||||
TableDateCompareTypeEnum,
|
||||
TableNumberCompareTypeEnum,
|
||||
TableStringCompareTypeEnum,
|
||||
} from '../../../types/enum';
|
||||
import {
|
||||
excelSerialToDateTime,
|
||||
getCellValueWithConditionType,
|
||||
getConditionExecuteFunc,
|
||||
getStringFromDataStream,
|
||||
isDateDynamicFilter,
|
||||
isNumberDynamicFilter,
|
||||
} from '../condition';
|
||||
import {
|
||||
dateM1,
|
||||
dateM10,
|
||||
dateM11,
|
||||
dateM12,
|
||||
dateM2,
|
||||
dateM3,
|
||||
dateM4,
|
||||
dateM5,
|
||||
dateM6,
|
||||
dateM7,
|
||||
dateM8,
|
||||
dateM9,
|
||||
dateQ1,
|
||||
dateQ2,
|
||||
dateQ3,
|
||||
dateQ4,
|
||||
getDateFilterExecuteFunc,
|
||||
lastQuarter,
|
||||
lastWeek,
|
||||
lastYear,
|
||||
nextMonth,
|
||||
nextQuarter,
|
||||
nextWeek,
|
||||
nextYear,
|
||||
thisMonth,
|
||||
thisQuarter,
|
||||
thisWeek,
|
||||
thisYear,
|
||||
today,
|
||||
tomorrow,
|
||||
yearToDate,
|
||||
yesterday,
|
||||
} from '../date-filter-util';
|
||||
import { above, below, getBottomN, getNumberFilterExecuteFunc, getTopN } from '../number-filter-util';
|
||||
import {
|
||||
getTextFilterExecuteFunc,
|
||||
textContain,
|
||||
textEndWith,
|
||||
textEqual,
|
||||
textNotContain,
|
||||
textNotEqual,
|
||||
textStartWith,
|
||||
} from '../text-filter-util';
|
||||
import { getLargestK, getSmallestK } from '../top-n';
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'filter-util-workbook',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'filter-util-workbook',
|
||||
sheetOrder: ['sheet-1'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
'sheet-1': {
|
||||
id: 'sheet-1',
|
||||
name: 'Sheet-1',
|
||||
rowCount: 10,
|
||||
columnCount: 8,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'plain text', t: CellValueType.STRING },
|
||||
1: { v: 'true', t: CellValueType.BOOLEAN },
|
||||
2: { v: 1, t: CellValueType.BOOLEAN },
|
||||
3: { v: true },
|
||||
4: { p: { body: { dataStream: 'rich text\r\n' } } as any },
|
||||
},
|
||||
1: {
|
||||
0: { v: '42', t: CellValueType.NUMBER },
|
||||
1: { v: 44927, t: CellValueType.NUMBER },
|
||||
2: { v: undefined },
|
||||
3: { v: 0, t: CellValueType.NUMBER },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('table filter utilities', () => {
|
||||
let univer: Univer | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
univer?.dispose();
|
||||
univer = undefined;
|
||||
});
|
||||
|
||||
it('matches relative date filters from a fixed business anchor', () => {
|
||||
const anchor = new Date(2024, 5, 12, 12);
|
||||
|
||||
expect(today(new Date(2024, 5, 12, 8), anchor)).toBe(true);
|
||||
expect(yesterday(new Date(2024, 5, 11), anchor)).toBe(true);
|
||||
expect(tomorrow(new Date(2024, 5, 13), anchor)).toBe(true);
|
||||
expect(thisWeek(new Date(2024, 5, 10), anchor)).toBe(true);
|
||||
expect(lastWeek(new Date(2024, 5, 5), anchor)).toBe(true);
|
||||
expect(nextWeek(new Date(2024, 5, 17), anchor)).toBe(true);
|
||||
expect(thisMonth(new Date(2024, 5, 1), anchor)).toBe(true);
|
||||
expect(nextMonth(new Date(2024, 6, 1), anchor)).toBe(true);
|
||||
expect(nextMonth(new Date(2024, 7, 1), anchor)).toBe(false);
|
||||
expect(thisQuarter(new Date(2024, 3, 1), anchor)).toBe(true);
|
||||
expect(lastQuarter(new Date(2024, 1, 15), anchor)).toBe(true);
|
||||
expect(nextQuarter(new Date(2024, 6, 15), anchor)).toBe(true);
|
||||
expect(thisYear(new Date(2024, 0, 1), anchor)).toBe(true);
|
||||
expect(lastYear(new Date(2023, 11, 31), anchor)).toBe(true);
|
||||
expect(nextYear(new Date(2025, 0, 1), anchor)).toBe(true);
|
||||
expect(yearToDate(new Date(2024, 2, 1), anchor)).toBe(true);
|
||||
expect(yearToDate(new Date(2024, 5, 13), anchor)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches date month and quarter presets used by table filter menus', () => {
|
||||
const monthChecks = [
|
||||
dateM1,
|
||||
dateM2,
|
||||
dateM3,
|
||||
dateM4,
|
||||
dateM5,
|
||||
dateM6,
|
||||
dateM7,
|
||||
dateM8,
|
||||
dateM9,
|
||||
dateM10,
|
||||
dateM11,
|
||||
dateM12,
|
||||
];
|
||||
|
||||
monthChecks.forEach((matchMonth, month) => {
|
||||
expect(matchMonth(new Date(2024, month, 1))).toBe(true);
|
||||
expect(matchMonth(new Date(2024, (month + 1) % 12, 1))).toBe(false);
|
||||
});
|
||||
|
||||
expect(dateQ1(new Date(2024, 1, 1))).toBe(true);
|
||||
expect(dateQ2(new Date(2024, 4, 1))).toBe(true);
|
||||
expect(dateQ3(new Date(2024, 7, 1))).toBe(true);
|
||||
expect(dateQ4(new Date(2024, 10, 1))).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves spreadsheet calendar components in western time zones', () => {
|
||||
vi.stubEnv('TZ', 'America/Los_Angeles');
|
||||
try {
|
||||
const date = excelSerialToDateTime(0, DateSystem.Date1904);
|
||||
|
||||
expect([date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()]).toEqual([1904, 0, 1, 0]);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it('builds executable date predicates for fixed and relative date filters', () => {
|
||||
const anchor = new Date(2024, 5, 12).getTime();
|
||||
const onJuneTen = new Date(2024, 5, 10);
|
||||
const onJuneTwelve = new Date(2024, 5, 12);
|
||||
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Equal,
|
||||
expectedValue: onJuneTen,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NotEqual,
|
||||
expectedValue: onJuneTen,
|
||||
})(onJuneTwelve)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.After,
|
||||
expectedValue: onJuneTen,
|
||||
})(onJuneTwelve)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.AfterOrEqual,
|
||||
expectedValue: onJuneTen,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Before,
|
||||
expectedValue: onJuneTwelve,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.BeforeOrEqual,
|
||||
expectedValue: onJuneTwelve,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Between,
|
||||
expectedValue: [onJuneTen, onJuneTwelve],
|
||||
})(new Date(2024, 5, 11))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NotBetween,
|
||||
expectedValue: [onJuneTen, onJuneTwelve],
|
||||
})(new Date(2024, 5, 14))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Today,
|
||||
anchorTime: anchor,
|
||||
})(onJuneTwelve)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Yesterday,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 5, 11))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Tomorrow,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 5, 13))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.ThisWeek,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 5, 10))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.LastWeek,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 5, 5))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NextWeek,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 5, 17))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.ThisMonth,
|
||||
anchorTime: anchor,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.LastMonth,
|
||||
anchorTime: anchor,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NextMonth,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 6, 1))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.ThisQuarter,
|
||||
anchorTime: anchor,
|
||||
})(onJuneTen)).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.LastQuarter,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 1, 15))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NextQuarter,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 6, 15))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.ThisYear,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 0, 1))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.LastYear,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2023, 0, 1))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.NextYear,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2025, 0, 1))).toBe(true);
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.YearToDate,
|
||||
anchorTime: anchor,
|
||||
})(new Date(2024, 2, 1))).toBe(true);
|
||||
[
|
||||
TableDateCompareTypeEnum.M1,
|
||||
TableDateCompareTypeEnum.M2,
|
||||
TableDateCompareTypeEnum.M3,
|
||||
TableDateCompareTypeEnum.M4,
|
||||
TableDateCompareTypeEnum.M5,
|
||||
TableDateCompareTypeEnum.M6,
|
||||
TableDateCompareTypeEnum.M7,
|
||||
TableDateCompareTypeEnum.M8,
|
||||
TableDateCompareTypeEnum.M9,
|
||||
TableDateCompareTypeEnum.M10,
|
||||
TableDateCompareTypeEnum.M11,
|
||||
TableDateCompareTypeEnum.M12,
|
||||
].forEach((compareType, month) => {
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType,
|
||||
})(new Date(2024, month, 1))).toBe(true);
|
||||
});
|
||||
expect(getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Q2,
|
||||
})(onJuneTwelve)).toBe(true);
|
||||
expect(() => getDateFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: 'unsupported' as TableDateCompareTypeEnum,
|
||||
})).toThrow('Unsupported compare type');
|
||||
});
|
||||
|
||||
it('matches numeric filters including ranges, averages and top n lists', () => {
|
||||
expect(above(8, 5)).toBe(true);
|
||||
expect(below(3, 5)).toBe(true);
|
||||
expect(getTopN([9, 1, 5, 3], 2, 9)).toBe(true);
|
||||
expect(getBottomN([9, 1, 5, 3], 2, 1)).toBe(true);
|
||||
expect(getLargestK([9, 1, 5, 3], 2).sort((a, b) => a - b)).toEqual([5, 9]);
|
||||
expect(getSmallestK([9, 1, 5, 3], 2).sort((a, b) => a - b)).toEqual([1, 3]);
|
||||
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.Equal,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(10)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.NotEqual,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(9)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.GreaterThanOrEqual,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(10)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.GreaterThan,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(11)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.LessThan,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(9)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.LessThanOrEqual,
|
||||
expectedValue: 10,
|
||||
}, undefined)!(10)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.Between,
|
||||
expectedValue: [20, 10],
|
||||
}, undefined)!(15)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.NotBetween,
|
||||
expectedValue: [10, 20],
|
||||
}, undefined)!(25)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.Above,
|
||||
expectedValue: 0,
|
||||
}, { average: 12 })!(13)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.Below,
|
||||
expectedValue: 0,
|
||||
}, { average: 12 })!(11)).toBe(true);
|
||||
expect(getNumberFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.TopN,
|
||||
expectedValue: 2,
|
||||
}, { list: [10, 30, 20] })!(30)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches text filters with wildcard-aware predicates', () => {
|
||||
expect(textEqual('North-01', 'North-*')).toBe(true);
|
||||
expect(textNotEqual('South-01', 'North-*')).toBe(true);
|
||||
expect(textContain('FY24 Revenue', 'Revenue')).toBe(true);
|
||||
expect(textNotContain('FY24 Cost', 'Revenue')).toBe(true);
|
||||
expect(textStartWith('Table Sales', 'Table')).toBe(true);
|
||||
expect(textEndWith('Table Sales', 'Sales')).toBe(true);
|
||||
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.Contains,
|
||||
expectedValue: 'West',
|
||||
})('West Region')).toBe(true);
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.Equal,
|
||||
expectedValue: 'West*',
|
||||
})('West Region')).toBe(true);
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.NotEqual,
|
||||
expectedValue: 'East*',
|
||||
})('West Region')).toBe(true);
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.NotContains,
|
||||
expectedValue: 'East',
|
||||
})('West Region')).toBe(true);
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.EndsWith,
|
||||
expectedValue: 'Region',
|
||||
})('West Region')).toBe(true);
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(getTextFilterExecuteFunc({
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: 'unknown' as TableStringCompareTypeEnum,
|
||||
expectedValue: 'x',
|
||||
})('anything')).toBe(true);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects condition filter executors and reads typed worksheet values from real workbook data', () => {
|
||||
univer = new Univer();
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
|
||||
const worksheet = workbook.getSheetBySheetId('sheet-1')!;
|
||||
|
||||
expect(isNumberDynamicFilter(TableNumberCompareTypeEnum.TopN)).toBe(true);
|
||||
expect(isNumberDynamicFilter(TableNumberCompareTypeEnum.Equal)).toBe(false);
|
||||
expect(isDateDynamicFilter(TableDateCompareTypeEnum.ThisWeek)).toBe(true);
|
||||
expect(isDateDynamicFilter(TableDateCompareTypeEnum.Equal)).toBe(false);
|
||||
|
||||
expect((getConditionExecuteFunc({
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.TopN,
|
||||
expectedValue: 2,
|
||||
},
|
||||
}, { list: [1, 2, 3] }) as (value: unknown) => boolean)(999)).toBe(true);
|
||||
expect((getConditionExecuteFunc({
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.String,
|
||||
compareType: TableStringCompareTypeEnum.StartsWith,
|
||||
expectedValue: 'plain',
|
||||
},
|
||||
}, undefined) as (value: unknown) => boolean)('plain text')).toBe(true);
|
||||
expect((getConditionExecuteFunc({
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Number,
|
||||
compareType: TableNumberCompareTypeEnum.Equal,
|
||||
expectedValue: 42,
|
||||
},
|
||||
}, undefined) as (value: unknown) => boolean)(42)).toBe(true);
|
||||
expect((getConditionExecuteFunc({
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Date,
|
||||
compareType: TableDateCompareTypeEnum.Equal,
|
||||
expectedValue: new Date(2024, 5, 12),
|
||||
},
|
||||
}, undefined) as (value: unknown) => boolean)(new Date(2024, 5, 12))).toBe(true);
|
||||
expect((getConditionExecuteFunc({
|
||||
filterType: TableColumnFilterTypeEnum.condition,
|
||||
filterInfo: {
|
||||
conditionType: TableConditionTypeEnum.Logic,
|
||||
compareType: TableNumberCompareTypeEnum.Equal,
|
||||
expectedValue: [],
|
||||
},
|
||||
}, undefined) as (value: unknown) => boolean)('any')).toBe(true);
|
||||
|
||||
expect(getStringFromDataStream({ body: { dataStream: 'rich text\r\n' } } as any)).toBe('rich text');
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 0, TableConditionTypeEnum.String)).toBe('plain text');
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 1, TableConditionTypeEnum.String)).toBe('TRUE');
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 2, TableConditionTypeEnum.String)).toBe('TRUE');
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 3, TableConditionTypeEnum.String)).toBe('TRUE');
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 4, TableConditionTypeEnum.String)).toBe('rich text');
|
||||
expect(getCellValueWithConditionType(worksheet, 1, 0, TableConditionTypeEnum.Number)).toBe(42);
|
||||
expect(getCellValueWithConditionType(worksheet, 0, 4, TableConditionTypeEnum.Number)).toBeNull();
|
||||
expect(getCellValueWithConditionType(worksheet, 1, 2, TableConditionTypeEnum.String)).toBeUndefined();
|
||||
expect(getCellValueWithConditionType(worksheet, 1, 1, TableConditionTypeEnum.Date)).toEqual(excelSerialToDateTime(44927));
|
||||
const serialZeroDate = getCellValueWithConditionType(worksheet, 1, 3, TableConditionTypeEnum.Date, DateSystem.Date1904);
|
||||
expect(serialZeroDate).toBeInstanceOf(Date);
|
||||
expect(serialZeroDate).toEqual(new Date(1904, 0, 1));
|
||||
expect(excelSerialToDateTime(0, DateSystem.Date1904)).toEqual(new Date(1904, 0, 1));
|
||||
expect(getCellValueWithConditionType(worksheet, 9, 9, TableConditionTypeEnum.String)).toBeNull();
|
||||
});
|
||||
});
|
||||
161
packages/sheets-table/src/models/filter-util/condition.ts
Normal file
161
packages/sheets-table/src/models/filter-util/condition.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 { IDocumentData, Worksheet } from '@univerjs/core';
|
||||
import type { TableStringCompareTypeEnum } from '../../types/enum';
|
||||
import type { ICalculatedOptions, ITableConditionFilterItem, ITableDateFilterInfo, ITableNumberFilterInfo, ITableStringFilterInfo } from '../../types/type';
|
||||
import { CellValueType, DateSystem, excelSerialToDateTime } from '@univerjs/core';
|
||||
import { TableConditionTypeEnum, TableDateCompareTypeEnum, TableNumberCompareTypeEnum } from '../../types/enum';
|
||||
import { getDateFilterExecuteFunc } from './date-filter-util';
|
||||
import { getNumberFilterExecuteFunc } from './number-filter-util';
|
||||
import { getTextFilterExecuteFunc } from './text-filter-util';
|
||||
|
||||
type TableConditionCompareType = TableNumberCompareTypeEnum | TableDateCompareTypeEnum | TableStringCompareTypeEnum;
|
||||
|
||||
// A dynamic filter returns a result set which might vary due to a change in the data itself or a change in the date on which the filter is being applied
|
||||
const NumberDynamicFilterCompareTypeSet: Set<TableNumberCompareTypeEnum> = new Set([
|
||||
TableNumberCompareTypeEnum.Above,
|
||||
TableNumberCompareTypeEnum.Below,
|
||||
TableNumberCompareTypeEnum.TopN,
|
||||
]);
|
||||
|
||||
const DateDynamicFilterCompareTypeSet: Set<TableDateCompareTypeEnum> = new Set([
|
||||
TableDateCompareTypeEnum.Today,
|
||||
TableDateCompareTypeEnum.Yesterday,
|
||||
TableDateCompareTypeEnum.Tomorrow,
|
||||
TableDateCompareTypeEnum.ThisWeek,
|
||||
TableDateCompareTypeEnum.LastWeek,
|
||||
TableDateCompareTypeEnum.NextWeek,
|
||||
TableDateCompareTypeEnum.ThisMonth,
|
||||
TableDateCompareTypeEnum.LastMonth,
|
||||
TableDateCompareTypeEnum.NextMonth,
|
||||
TableDateCompareTypeEnum.ThisQuarter,
|
||||
TableDateCompareTypeEnum.LastQuarter,
|
||||
TableDateCompareTypeEnum.NextQuarter,
|
||||
TableDateCompareTypeEnum.NextYear,
|
||||
TableDateCompareTypeEnum.ThisYear,
|
||||
TableDateCompareTypeEnum.LastYear,
|
||||
TableDateCompareTypeEnum.YearToDate,
|
||||
]);
|
||||
|
||||
export function isNumberDynamicFilter(compareType: TableConditionCompareType): compareType is TableNumberCompareTypeEnum {
|
||||
return NumberDynamicFilterCompareTypeSet.has(compareType as TableNumberCompareTypeEnum);
|
||||
}
|
||||
|
||||
export function isDateDynamicFilter(compareType: TableConditionCompareType): compareType is TableDateCompareTypeEnum {
|
||||
return DateDynamicFilterCompareTypeSet.has(compareType as TableDateCompareTypeEnum);
|
||||
}
|
||||
|
||||
export function getConditionExecuteFunc(filter: ITableConditionFilterItem, calculatedOptions: ICalculatedOptions | undefined) {
|
||||
if (isNumberDynamicFilter(filter.filterInfo.compareType)) {
|
||||
return (value: any) => true;
|
||||
} else {
|
||||
switch (filter.filterInfo.conditionType) {
|
||||
case TableConditionTypeEnum.Date:
|
||||
return getDateFilterExecuteFunc(filter.filterInfo as ITableDateFilterInfo);
|
||||
case TableConditionTypeEnum.Number:
|
||||
return getNumberFilterExecuteFunc(filter.filterInfo as ITableNumberFilterInfo, calculatedOptions);
|
||||
case TableConditionTypeEnum.String:
|
||||
return getTextFilterExecuteFunc(filter.filterInfo as ITableStringFilterInfo);
|
||||
case TableConditionTypeEnum.Logic:
|
||||
default:
|
||||
return (value: any) => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCellValueWithConditionType(
|
||||
sheet: Worksheet,
|
||||
row: number,
|
||||
col: number,
|
||||
conditionType: TableConditionTypeEnum,
|
||||
dateSystem = DateSystem.Date1900
|
||||
) {
|
||||
switch (conditionType) {
|
||||
case TableConditionTypeEnum.Date:
|
||||
{
|
||||
const dateNumber = getNumberCellValue(sheet, row, col);
|
||||
return dateNumber == null ? null : excelSerialToDateTimeLocal(dateNumber, dateSystem);
|
||||
}
|
||||
case TableConditionTypeEnum.Number:
|
||||
return getNumberCellValue(sheet, row, col);
|
||||
case TableConditionTypeEnum.String:
|
||||
default:
|
||||
return getStringCellValue(sheet, row, col);
|
||||
}
|
||||
}
|
||||
|
||||
export const getStringFromDataStream = (data: IDocumentData): string => {
|
||||
const dataStream = data.body?.dataStream.replace(/\r\n$/, '') || '';
|
||||
return dataStream;
|
||||
};
|
||||
|
||||
function getStringCellValue(sheet: Worksheet, row: number, col: number) {
|
||||
const cellData = sheet.getCell(row, col);
|
||||
if (!cellData) return null;
|
||||
const { v, t, p } = cellData;
|
||||
if (p) {
|
||||
return getStringFromDataStream(p);
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
if (t === CellValueType.BOOLEAN) return v.toUpperCase();
|
||||
return v;
|
||||
};
|
||||
|
||||
if (typeof v === 'number') {
|
||||
if (t === CellValueType.BOOLEAN) return v ? 'TRUE' : 'FALSE';
|
||||
return v;
|
||||
};
|
||||
|
||||
if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
|
||||
|
||||
if (v === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return String(v);
|
||||
}
|
||||
function getNumberCellValue(sheet: Worksheet, row: number, col: number) {
|
||||
const cellData = sheet.getCell(row, col);
|
||||
if (!cellData) return null;
|
||||
const { v, t, p } = cellData;
|
||||
if (p) {
|
||||
return null;
|
||||
}
|
||||
if (typeof v === 'string' && t === CellValueType.NUMBER) {
|
||||
return Number(sheet.getCellRaw(row, col)!.v);
|
||||
}
|
||||
return Number(v);
|
||||
}
|
||||
|
||||
function excelSerialToDateTimeLocal(serial: number, dateSystem = DateSystem.Date1900): Date {
|
||||
const utcDate = excelSerialToDateTime(serial, dateSystem);
|
||||
if (Number.isNaN(utcDate.getTime())) {
|
||||
return utcDate;
|
||||
}
|
||||
// Table date predicates use local Date components; keep the spreadsheet fields stable across host time zones.
|
||||
return new Date(
|
||||
utcDate.getUTCFullYear(),
|
||||
utcDate.getUTCMonth(),
|
||||
utcDate.getUTCDate(),
|
||||
utcDate.getUTCHours(),
|
||||
utcDate.getUTCMinutes(),
|
||||
utcDate.getUTCSeconds(),
|
||||
utcDate.getUTCMilliseconds()
|
||||
);
|
||||
}
|
||||
|
||||
export { excelSerialToDateTimeLocal as excelSerialToDateTime };
|
||||
577
packages/sheets-table/src/models/filter-util/date-filter-util.ts
Normal file
577
packages/sheets-table/src/models/filter-util/date-filter-util.ts
Normal file
@@ -0,0 +1,577 @@
|
||||
/**
|
||||
* 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 { ITableDateFilterInfo } from '../../types/type';
|
||||
import { TableDateCompareTypeEnum } from '../../types/enum';
|
||||
|
||||
/**
|
||||
* The provided date is a date in Q1 of the year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateQ1 = (date: Date): boolean => {
|
||||
const month = date.getMonth();
|
||||
return month <= 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in Q2 of the year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateQ2 = (date: Date): boolean => {
|
||||
const month = date.getMonth();
|
||||
return month > 2 && month <= 5;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in Q3 of the year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateQ3 = (date: Date): boolean => {
|
||||
const month = date.getMonth();
|
||||
return month > 5 && month <= 8;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in Q4 of the year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateQ4 = (date: Date): boolean => {
|
||||
const month = date.getMonth();
|
||||
return month > 8 && month <= 11;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in January.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM1 = (date: Date): boolean => {
|
||||
return date.getMonth() === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in February.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM2 = (date: Date): boolean => {
|
||||
return date.getMonth() === 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in March.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM3 = (date: Date): boolean => {
|
||||
return date.getMonth() === 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in April.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM4 = (date: Date): boolean => {
|
||||
return date.getMonth() === 3;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in May.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM5 = (date: Date): boolean => {
|
||||
return date.getMonth() === 4;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in June.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM6 = (date: Date): boolean => {
|
||||
return date.getMonth() === 5;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in July.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM7 = (date: Date): boolean => {
|
||||
return date.getMonth() === 6;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in August.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM8 = (date: Date): boolean => {
|
||||
return date.getMonth() === 7;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in September.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM9 = (date: Date): boolean => {
|
||||
return date.getMonth() === 8;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in October.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM10 = (date: Date): boolean => {
|
||||
return date.getMonth() === 9;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in November.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM11 = (date: Date): boolean => {
|
||||
return date.getMonth() === 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is a date in December.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const dateM12 = (date: Date): boolean => {
|
||||
return date.getMonth() === 11;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is today.
|
||||
* @param {Date} expectedDate - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const today = (expectedDate: Date, anchorTime: Date = new Date()): boolean => {
|
||||
return expectedDate.toDateString() === anchorTime.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is tomorrow.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const tomorrow = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const tomorrow = new Date(anchorTime);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return date.toDateString() === tomorrow.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is yesterday.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const yesterday = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const yesterday = new Date(anchorTime);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return date.toDateString() === yesterday.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the start date of the week for the provided date.
|
||||
* @param {Date} date - The date to get the week start.
|
||||
* @returns {Date} The start date of the week.
|
||||
*/
|
||||
const getWeekStart = (date: Date): Date => {
|
||||
const day = date.getDay();
|
||||
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
|
||||
const weekStart = new Date(date);
|
||||
weekStart.setDate(diff);
|
||||
return weekStart;
|
||||
};
|
||||
|
||||
const perWeek = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* The provided date is in the current week.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const thisWeek = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const weekStart = getWeekStart(date);
|
||||
const anchorTimeWeekStart = getWeekStart(anchorTime);
|
||||
return weekStart.toDateString() === anchorTimeWeekStart.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the next week.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const nextWeek = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const weekStart = getWeekStart(date);
|
||||
const anchorTimeNextWeekStart = new Date(getWeekStart(anchorTime).getTime() + perWeek);
|
||||
return weekStart.toDateString() === anchorTimeNextWeekStart.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the last week.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const lastWeek = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const weekStart = getWeekStart(date);
|
||||
const anchorTimeLastWeekStart = new Date(getWeekStart(anchorTime).getTime() - perWeek);
|
||||
return weekStart.toDateString() === anchorTimeLastWeekStart.toDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the current month.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const thisMonth = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
return date.getFullYear() === anchorTime.getFullYear() && date.getMonth() === anchorTime.getMonth();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the start date of the month for the provided date.
|
||||
* @param {Date} date - The date to get the month start.
|
||||
* @returns {Date} The start date of the month.
|
||||
*/
|
||||
const getMonthStart = (date: Date): Date => {
|
||||
const monthStart = new Date(date);
|
||||
monthStart.setHours(0, 0, 0, 0);
|
||||
monthStart.setDate(1);
|
||||
return monthStart;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the next month.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const nextMonth = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const nextMonthStart = new Date(anchorTime);
|
||||
nextMonthStart.setHours(0, 0, 0, 0);
|
||||
nextMonthStart.setMonth(nextMonthStart.getMonth() + 1, 1);
|
||||
|
||||
const monthEnd = new Date(nextMonthStart);
|
||||
monthEnd.setMonth(monthEnd.getMonth() + 1, 0);
|
||||
|
||||
const dateTime = date.getTime();
|
||||
|
||||
return dateTime >= nextMonthStart.getTime() && dateTime < monthEnd.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the last month.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const lastMonth = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const lastMonthStart = getMonthStart(anchorTime);
|
||||
|
||||
const monthEnd = new Date(lastMonthStart);
|
||||
monthEnd.setMonth(monthEnd.getMonth() + 1, 0);
|
||||
|
||||
const dateTime = date.getTime();
|
||||
|
||||
return dateTime >= lastMonthStart.getTime() && dateTime < monthEnd.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the start date of the quarter for the provided date.
|
||||
* @param {Date} date - The date to get the quarter start.
|
||||
* @returns {Date} The start date of the quarter.
|
||||
*/
|
||||
const getQuarterStart = (date: Date): Date => {
|
||||
const quarterStart = new Date(date);
|
||||
quarterStart.setHours(0, 0, 0, 0);
|
||||
quarterStart.setDate(1);
|
||||
const month = quarterStart.getMonth();
|
||||
quarterStart.setMonth(month - month % 3);
|
||||
return quarterStart;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the current quarter.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const thisQuarter = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const quarterStart = getQuarterStart(anchorTime);
|
||||
const nextQuarterStart = new Date(quarterStart);
|
||||
nextQuarterStart.setMonth(nextQuarterStart.getMonth() + 3);
|
||||
const dateTime = date.getTime();
|
||||
return dateTime >= quarterStart.getTime() && dateTime < nextQuarterStart.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the next quarter.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const nextQuarter = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const quarterStart = getQuarterStart(anchorTime);
|
||||
const nextQuarterStart = new Date(quarterStart);
|
||||
nextQuarterStart.setMonth(nextQuarterStart.getMonth() + 3);
|
||||
const nextQuarterEnd = new Date(nextQuarterStart);
|
||||
nextQuarterEnd.setMonth(nextQuarterEnd.getMonth() + 3, 0);
|
||||
|
||||
const dateTime = date.getTime();
|
||||
|
||||
return dateTime >= nextQuarterStart.getTime() && dateTime < nextQuarterEnd.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the last quarter.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const lastQuarter = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const quarterStart = getQuarterStart(anchorTime);
|
||||
const lastQuarterStart = new Date(quarterStart);
|
||||
lastQuarterStart.setMonth(lastQuarterStart.getMonth() - 3);
|
||||
const lastQuarterEnd = new Date(quarterStart);
|
||||
lastQuarterEnd.setDate(0);
|
||||
|
||||
const dateTime = date.getTime();
|
||||
|
||||
return dateTime >= lastQuarterStart.getTime() && dateTime < lastQuarterEnd.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the current year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const thisYear = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
return date.getFullYear() === anchorTime.getFullYear();
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the next year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const nextYear = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
return date.getFullYear() === anchorTime.getFullYear() + 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the last year.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const lastYear = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
return date.getFullYear() === anchorTime.getFullYear() - 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The provided date is in the year to date.
|
||||
* @param {Date} date - The date to compare.
|
||||
* @param {Date} [anchorTime] - The reference date.
|
||||
* @returns {boolean} return the date is match
|
||||
*/
|
||||
export const yearToDate = (date: Date, anchorTime: Date = new Date()): boolean => {
|
||||
const yearStart = new Date(anchorTime);
|
||||
yearStart.setHours(0, 0, 0, 0);
|
||||
yearStart.setMonth(0, 1);
|
||||
const dateTime = date.getTime();
|
||||
return dateTime >= yearStart.getTime() && dateTime < anchorTime.getTime();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
export function getDateFilterExecuteFunc(filterInfo: ITableDateFilterInfo) {
|
||||
switch (filterInfo.compareType) {
|
||||
case TableDateCompareTypeEnum.Equal:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() === expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.NotEqual:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() !== expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.After:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() > expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.Before:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() < expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.AfterOrEqual:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() >= expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.BeforeOrEqual:
|
||||
{
|
||||
const expected = new Date(filterInfo.expectedValue as string | Date);
|
||||
return (date: Date) => date.getTime() <= expected.getTime();
|
||||
}
|
||||
case TableDateCompareTypeEnum.Between:
|
||||
return (date: Date) => {
|
||||
const [start, end] = filterInfo.expectedValue as [Date, Date];
|
||||
return date.getTime() >= new Date(start).getTime() && date.getTime() <= new Date(end).getTime();
|
||||
};
|
||||
case TableDateCompareTypeEnum.NotBetween:
|
||||
return (date: Date) => {
|
||||
const [start, end] = filterInfo.expectedValue as [Date, Date];
|
||||
return date.getTime() < new Date(start).getTime() || date.getTime() > new Date(end).getTime();
|
||||
};
|
||||
case TableDateCompareTypeEnum.Today:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => today(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.Yesterday:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => yesterday(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.Tomorrow:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => tomorrow(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.ThisWeek:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => thisWeek(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.LastWeek:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => lastWeek(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.NextWeek:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => nextWeek(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.ThisMonth:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => thisMonth(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.LastMonth:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => lastMonth(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.NextMonth:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => nextMonth(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.ThisQuarter:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => thisQuarter(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.LastQuarter:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => lastQuarter(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.NextQuarter:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => nextQuarter(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.ThisYear:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => thisYear(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.LastYear:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => lastYear(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.NextYear:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => nextYear(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.YearToDate:
|
||||
{
|
||||
const anchorTime = filterInfo.anchorTime ? new Date(filterInfo.anchorTime) : new Date();
|
||||
return (date: Date) => yearToDate(date, anchorTime);
|
||||
}
|
||||
case TableDateCompareTypeEnum.M1:
|
||||
return dateM1;
|
||||
case TableDateCompareTypeEnum.M2:
|
||||
return dateM2;
|
||||
case TableDateCompareTypeEnum.M3:
|
||||
return dateM3;
|
||||
case TableDateCompareTypeEnum.M4:
|
||||
return dateM4;
|
||||
case TableDateCompareTypeEnum.M5:
|
||||
return dateM5;
|
||||
case TableDateCompareTypeEnum.M6:
|
||||
return dateM6;
|
||||
case TableDateCompareTypeEnum.M7:
|
||||
return dateM7;
|
||||
case TableDateCompareTypeEnum.M8:
|
||||
return dateM8;
|
||||
case TableDateCompareTypeEnum.M9:
|
||||
return dateM9;
|
||||
case TableDateCompareTypeEnum.M10:
|
||||
return dateM10;
|
||||
case TableDateCompareTypeEnum.M11:
|
||||
return dateM11;
|
||||
case TableDateCompareTypeEnum.M12:
|
||||
return dateM12;
|
||||
case TableDateCompareTypeEnum.Q1:
|
||||
return dateQ1;
|
||||
case TableDateCompareTypeEnum.Q2:
|
||||
return dateQ2;
|
||||
case TableDateCompareTypeEnum.Q3:
|
||||
return dateQ3;
|
||||
case TableDateCompareTypeEnum.Q4:
|
||||
return dateQ4;
|
||||
default:
|
||||
throw new Error('Unsupported compare type');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 { ICalculatedOptions, ITableNumberFilterInfo } from '../../types/type';
|
||||
import { TableNumberCompareTypeEnum } from '../../types/enum';
|
||||
import { getLargestK, getSmallestK } from './top-n';
|
||||
|
||||
/**
|
||||
* @description Checks if a value is above the average.
|
||||
* @param {number} value - The value to check.
|
||||
* @param {number} average - The average value.
|
||||
* @returns {boolean} A boolean value indicating whether the value is above the average.
|
||||
*/
|
||||
export const above = (value: number, average: number): boolean => {
|
||||
return value > average;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if a value is below the average.
|
||||
* @param {number} value - The value to check.
|
||||
* @param {number} average - The average value.
|
||||
* @returns {boolean} A boolean value indicating whether the value is below the average.
|
||||
*/
|
||||
export const below = (value: number, average: number): boolean => {
|
||||
return value < average;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Gets the largest N values from a list and checks if the expected value is included.
|
||||
* @param {number[]} list - The list of numbers.
|
||||
* @param {number} top - The number of top values to retrieve.
|
||||
* @param {number} expectedValue - The expected value to check for inclusion.
|
||||
* @returns {boolean} A boolean value indicating whether the expected value is included in the top N values.
|
||||
*/
|
||||
export const getTopN = (list: number[], top: number, expectedValue: number): boolean => {
|
||||
const heap = getLargestK(list, top);
|
||||
return heap.includes(expectedValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Gets the smallest N values from a list and checks if the expected value is included.
|
||||
* @param {number[]} list - The list of numbers.
|
||||
* @param {number} bottom - The number of bottom values to retrieve.
|
||||
* @param {number} expectedValue - The expected value to check for inclusion.
|
||||
* @returns {boolean} A boolean value indicating whether the expected value is included in the bottom N values.
|
||||
*/
|
||||
export const getBottomN = (list: number[], bottom: number, expectedValue: number): boolean => {
|
||||
const heap = getSmallestK(list, bottom);
|
||||
return heap.includes(expectedValue);
|
||||
};
|
||||
|
||||
export function getNumberFilterExecuteFunc(filter: ITableNumberFilterInfo, calculatedOptions: ICalculatedOptions | undefined) {
|
||||
switch (filter.compareType) {
|
||||
case TableNumberCompareTypeEnum.Equal:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value === expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.NotEqual:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value !== expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.GreaterThan:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value > expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.GreaterThanOrEqual:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value >= expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.LessThan:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value < expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.LessThanOrEqual:
|
||||
{
|
||||
const expectedValue = Number(filter.expectedValue);
|
||||
return (value: number) => value <= expectedValue;
|
||||
}
|
||||
case TableNumberCompareTypeEnum.Between:
|
||||
{
|
||||
const [min, max] = filter.expectedValue as [number, number];
|
||||
const minValue = Number(min);
|
||||
const maxValue = Number(max);
|
||||
if (minValue > maxValue) {
|
||||
return (value: number) => value >= maxValue && value <= minValue;
|
||||
}
|
||||
return (value: number) => value >= minValue && value <= maxValue;
|
||||
}
|
||||
|
||||
case TableNumberCompareTypeEnum.NotBetween:
|
||||
{
|
||||
const [min, max] = filter.expectedValue as [number, number];
|
||||
const minValue = Number(min);
|
||||
const maxValue = Number(max);
|
||||
if (minValue > maxValue) {
|
||||
return (value: number) => value < maxValue || value > minValue;
|
||||
}
|
||||
return (value: number) => value < minValue || value > maxValue;
|
||||
}
|
||||
|
||||
case TableNumberCompareTypeEnum.Above:
|
||||
{
|
||||
const average = calculatedOptions!.average as number;
|
||||
return (value: number) => above(value, average);
|
||||
}
|
||||
|
||||
case TableNumberCompareTypeEnum.Below:
|
||||
{
|
||||
const average = calculatedOptions!.average as number;
|
||||
return (value: number) => below(value, average);
|
||||
}
|
||||
|
||||
case TableNumberCompareTypeEnum.TopN:
|
||||
{
|
||||
const list = calculatedOptions!.list as number[];
|
||||
const top = Number(filter.expectedValue);
|
||||
|
||||
return (value: number) => getTopN(list, top, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { ITableStringFilterInfo } from '../../types/type';
|
||||
import { regexp } from '@univerjs/core';
|
||||
import { TableStringCompareTypeEnum } from '../../types/enum';
|
||||
|
||||
export const textEqual = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(expectedValue);
|
||||
return regex.test(compareValue);
|
||||
};
|
||||
|
||||
export const textNotEqual = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(expectedValue);
|
||||
return !regex.test(compareValue);
|
||||
};
|
||||
|
||||
export const textContain = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(`*${expectedValue}*`);
|
||||
return regex.test(compareValue);
|
||||
};
|
||||
|
||||
export const textNotContain = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(`*${expectedValue}*`);
|
||||
return !regex.test(compareValue);
|
||||
};
|
||||
|
||||
export const textStartWith = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(`${expectedValue}*`);
|
||||
return regex.test(compareValue);
|
||||
};
|
||||
|
||||
export const textEndWith = (compareValue: string, expectedValue: string) => {
|
||||
const regex = regexp.createFromWildcard(`*${expectedValue}`);
|
||||
return regex.test(compareValue);
|
||||
};
|
||||
|
||||
export function getTextFilterExecuteFunc(filter: ITableStringFilterInfo) {
|
||||
switch (filter.compareType) {
|
||||
case TableStringCompareTypeEnum.Equal:
|
||||
return (value: string) => textEqual(value, filter.expectedValue);
|
||||
case TableStringCompareTypeEnum.NotEqual:
|
||||
return (value: string) => textNotEqual(value, filter.expectedValue);
|
||||
case TableStringCompareTypeEnum.Contains:
|
||||
return (value: string) => textContain(value, filter.expectedValue);
|
||||
case TableStringCompareTypeEnum.NotContains:
|
||||
return (value: string) => textNotContain(value, filter.expectedValue);
|
||||
case TableStringCompareTypeEnum.StartsWith:
|
||||
return (value: string) => textStartWith(value, filter.expectedValue);
|
||||
case TableStringCompareTypeEnum.EndsWith:
|
||||
return (value: string) => textEndWith(value, filter.expectedValue);
|
||||
default:
|
||||
console.error(`Unknown filter operator: ${filter.compareType}`);
|
||||
return (value: string) => true;
|
||||
}
|
||||
}
|
||||
266
packages/sheets-table/src/models/filter-util/top-n.ts
Normal file
266
packages/sheets-table/src/models/filter-util/top-n.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description Represents a heap data structure.
|
||||
* A heap is a complete binary tree that satisfies the heap property.
|
||||
* The heap property states that the parent node is always smaller or bigger than its children.
|
||||
* The root node is the smallest or biggest element in the heap.
|
||||
* The heap is used to find the kth largest or smallest element in a list.
|
||||
* A array can be used to represent a heap.
|
||||
* The root node is at index 0, and the left child of a node at index i is at index 2i + 1.
|
||||
* The right child of a node at index i is at index 2i + 2.
|
||||
* The parent node of a node at index i is at index (i - 1) / 2.
|
||||
* @example
|
||||
* const maxHeap = [5, 3, 4, 1, 2];
|
||||
* // const minHeap = [6, 8, 7, 10, 9];
|
||||
* // the root node in maxHeap is 5
|
||||
* // the root child is 3, 4, then the child of 3 is 1, 2
|
||||
*/
|
||||
class Heap {
|
||||
heap: number[];
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Heap class.
|
||||
*/
|
||||
constructor() {
|
||||
this.heap = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the elements at the given indices in the heap.
|
||||
* @param index1 The index of the first element.
|
||||
* @param index2 The index of the second element.
|
||||
*/
|
||||
swap(index1: number, index2: number) {
|
||||
const temp = this.heap[index1];
|
||||
this.heap[index1] = this.heap[index2];
|
||||
this.heap[index2] = temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the parent node for the given index.
|
||||
* @param index The index of the node.
|
||||
* @returns The index of the parent node.
|
||||
*/
|
||||
getParentIndex(index: number) {
|
||||
return Math.floor((index - 1) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the left child node for the given index.
|
||||
* @param index The index of the node.
|
||||
* @returns The index of the left child node.
|
||||
*/
|
||||
getLeftIndex(index: number) {
|
||||
return index * 2 + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the right child node for the given index.
|
||||
* @param index The index of the node.
|
||||
* @returns The index of the right child node.
|
||||
*/
|
||||
getRightIndex(index: number) {
|
||||
return index * 2 + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in the heap.
|
||||
* @returns The number of elements in the heap.
|
||||
*/
|
||||
size(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum value in the heap without removing it.
|
||||
* @returns The minimum value in the heap.
|
||||
*/
|
||||
peek(): number {
|
||||
return this.heap[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns whether the heap includes the given value.
|
||||
* @param {number} value The value to be checked.
|
||||
* @returns {boolean} return true if the heap includes the given value
|
||||
*/
|
||||
include(value: number): boolean {
|
||||
return this.heap.includes(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Represents a min heap data structure.
|
||||
* in MinHeap, the parent node is always smaller than its children.
|
||||
* The root node is the smallest element in the heap.
|
||||
* The min heap is used to find the kth largest element in a list.
|
||||
*/
|
||||
class MinHeap extends Heap {
|
||||
/**
|
||||
* Initializes a new instance of the MinHeap class.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the element at the given index up the heap until it satisfies the min heap property.
|
||||
* @param index The index of the element to be shifted up.
|
||||
*/
|
||||
shiftUp(index: number) {
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
const parentIndex = this.getParentIndex(index);
|
||||
if (this.heap[parentIndex] > this.heap[index]) {
|
||||
this.swap(parentIndex, index);
|
||||
this.shiftUp(parentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the element at the given index down the heap until it satisfies the min heap property.
|
||||
* @param index The index of the element to be shifted down.
|
||||
*/
|
||||
shiftDown(index: number) {
|
||||
const leftIndex = this.getLeftIndex(index);
|
||||
const rightIndex = this.getRightIndex(index);
|
||||
|
||||
if (this.heap[leftIndex] < this.heap[index]) {
|
||||
this.swap(leftIndex, index);
|
||||
this.shiftDown(leftIndex);
|
||||
}
|
||||
|
||||
if (this.heap[rightIndex] < this.heap[index]) {
|
||||
this.swap(rightIndex, index);
|
||||
this.shiftDown(rightIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new value into the min heap.
|
||||
* @param value The value to be inserted.
|
||||
*/
|
||||
insert(value: number) {
|
||||
this.heap.push(value);
|
||||
this.shiftUp(this.heap.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the minimum value from the min heap.
|
||||
*/
|
||||
pop() {
|
||||
this.heap[0] = this.heap.pop() as number;
|
||||
this.shiftDown(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a max heap data structure.
|
||||
*/
|
||||
class MaxHeap extends Heap {
|
||||
/**
|
||||
* Initializes a new instance of the MaxHeap class.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the element at the given index up the heap until it satisfies the max heap property.
|
||||
* @param index The index of the element to be shifted up.
|
||||
*/
|
||||
shiftUp(index: number) {
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
const parentIndex = this.getParentIndex(index);
|
||||
if (this.heap[parentIndex] < this.heap[index]) {
|
||||
this.swap(parentIndex, index);
|
||||
this.shiftUp(parentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the element at the given index down the heap until it satisfies the max heap property.
|
||||
* @param index The index of the element to be shifted down.
|
||||
*/
|
||||
shiftDown(index: number) {
|
||||
const leftIndex = this.getLeftIndex(index);
|
||||
const rightIndex = this.getRightIndex(index);
|
||||
if (this.heap[leftIndex] > this.heap[index]) {
|
||||
this.swap(leftIndex, index);
|
||||
this.shiftDown(leftIndex);
|
||||
}
|
||||
if (this.heap[rightIndex] > this.heap[index]) {
|
||||
this.swap(rightIndex, index);
|
||||
this.shiftDown(rightIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new value into the max heap.
|
||||
* @param value The value to be inserted.
|
||||
*/
|
||||
insert(value: number) {
|
||||
this.heap.push(value);
|
||||
this.shiftUp(this.heap.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the maximum value from the max heap.
|
||||
*/
|
||||
pop() {
|
||||
this.heap[0] = this.heap.pop() as number;
|
||||
this.shiftDown(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the kth largest element from the given list using a min heap.
|
||||
* @param {number[]} list The list of numbers.
|
||||
* @param {number} k The value of k.
|
||||
* @returns The kth largest element.
|
||||
*/
|
||||
export const getLargestK = (list: number[], k: number) => {
|
||||
const minHeap = new MinHeap();
|
||||
for (const item of list) {
|
||||
minHeap.insert(item);
|
||||
if (minHeap.size() > k) {
|
||||
minHeap.pop();
|
||||
}
|
||||
}
|
||||
return minHeap.heap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the kth smallest element from the given list using a max heap.
|
||||
* @param {number[]} list The list of numbers.
|
||||
* @param {number} k The value of k.
|
||||
* @returns The kth smallest element.
|
||||
*/
|
||||
export const getSmallestK = (list: number[], k: number) => {
|
||||
const max = new MaxHeap();
|
||||
for (const item of list) {
|
||||
max.insert(item);
|
||||
if (max.size() > k) {
|
||||
max.pop();
|
||||
}
|
||||
}
|
||||
return max.heap;
|
||||
};
|
||||
68
packages/sheets-table/src/models/table-column.ts
Normal file
68
packages/sheets-table/src/models/table-column.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 { IStyleData } from '@univerjs/core';
|
||||
import type { TableMetaType } from '../types/type';
|
||||
import { TableColumnDataTypeEnum } from '../types/enum';
|
||||
|
||||
export class TableColumn {
|
||||
dataType: TableColumnDataTypeEnum;
|
||||
id: string;
|
||||
displayName: string;
|
||||
formula: string;
|
||||
meta: TableMetaType;
|
||||
style: IStyleData;
|
||||
constructor(id: string, name: string) {
|
||||
this.id = id;
|
||||
this.displayName = name;
|
||||
this.dataType = TableColumnDataTypeEnum.String;
|
||||
this.formula = '';
|
||||
this.meta = {};
|
||||
this.style = {};
|
||||
}
|
||||
|
||||
getMeta() {
|
||||
return this.meta;
|
||||
}
|
||||
|
||||
setMeta(meta: TableMetaType) {
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
displayName: this.displayName,
|
||||
dataType: this.dataType,
|
||||
formula: this.formula,
|
||||
meta: this.meta,
|
||||
style: this.style,
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json: any) {
|
||||
this.id = json.id;
|
||||
this.displayName = json.displayName;
|
||||
this.dataType = json.dataType;
|
||||
this.formula = json.formula;
|
||||
this.meta = json.meta;
|
||||
this.style = json.style;
|
||||
}
|
||||
}
|
||||
187
packages/sheets-table/src/models/table-filter.ts
Normal file
187
packages/sheets-table/src/models/table-filter.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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, IStyleData, Nullable, Worksheet } from '@univerjs/core';
|
||||
import type { SheetsTableButtonStateEnum } from '../types/enum';
|
||||
import type { ICalculatedOptions, ITableFilterItem, ITableFilterJSON, ITableRange } from '../types/type';
|
||||
import { ColorKit, DateSystem, DEFAULT_STYLES, getColorStyle } from '@univerjs/core';
|
||||
import { TABLE_FILTER_EMPTY_VALUE } from '../const';
|
||||
import { SheetsTableSortStateEnum, TableColumnFilterTypeEnum, TableConditionTypeEnum } from '../types/enum';
|
||||
import { getTableFilterState, isColorTableFilter, isConditionFilter } from '../util';
|
||||
import { getCellValueWithConditionType, getConditionExecuteFunc, isNumberDynamicFilter } from './filter-util/condition';
|
||||
|
||||
export class TableFilters {
|
||||
private _tableColumnFilterList: (ITableFilterItem | undefined)[];
|
||||
private _tableSortInfo?: { columnIndex: number; sortState: SheetsTableSortStateEnum };
|
||||
private _filterOutRows: Set<number>;
|
||||
constructor() {
|
||||
this._tableColumnFilterList = [];
|
||||
}
|
||||
|
||||
setColumnFilter(columnIndex: number, filter: ITableFilterItem | undefined) {
|
||||
if (!filter) {
|
||||
this._tableColumnFilterList[columnIndex] = undefined;
|
||||
} else {
|
||||
this._tableColumnFilterList[columnIndex] = filter;
|
||||
}
|
||||
}
|
||||
|
||||
setSortState(columnIndex: number, sortState: SheetsTableSortStateEnum) {
|
||||
this._tableSortInfo = { columnIndex, sortState };
|
||||
}
|
||||
|
||||
setSortInfo(sortInfo: ITableFilterJSON['tableSortInfo']) {
|
||||
this._tableSortInfo = sortInfo;
|
||||
}
|
||||
|
||||
getColumnFilter(columnIndex: number): ITableFilterItem | undefined {
|
||||
return this._tableColumnFilterList[columnIndex];
|
||||
}
|
||||
|
||||
getFilterState(columnIndex: number): SheetsTableButtonStateEnum {
|
||||
const sortState = this._tableSortInfo?.columnIndex === columnIndex ? this._tableSortInfo.sortState : SheetsTableSortStateEnum.None;
|
||||
return getTableFilterState(this._tableColumnFilterList[columnIndex], sortState);
|
||||
}
|
||||
|
||||
getSortState() {
|
||||
return this._tableSortInfo ?? {};
|
||||
}
|
||||
|
||||
getFilterStates(range: ITableRange): SheetsTableButtonStateEnum[] {
|
||||
const states = [];
|
||||
const { startColumn, endColumn } = range;
|
||||
for (let i = startColumn; i <= endColumn; i++) {
|
||||
states.push(this.getFilterState(i - startColumn));
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
getFilterOutRows() {
|
||||
return this._filterOutRows;
|
||||
}
|
||||
|
||||
doFilter(sheet: Worksheet, range: ITableRange, dateSystem = DateSystem.Date1900) {
|
||||
const filterOutRows = new Set<number>();
|
||||
const tableColumnFilterList = this._tableColumnFilterList;
|
||||
for (let i = 0; i < tableColumnFilterList.length; i++) {
|
||||
const filter = tableColumnFilterList[i];
|
||||
if (filter) {
|
||||
this.doColumnFilter(sheet, range, i, filterOutRows, dateSystem);
|
||||
}
|
||||
}
|
||||
this._filterOutRows = filterOutRows;
|
||||
return filterOutRows;
|
||||
}
|
||||
|
||||
doColumnFilter(sheet: Worksheet, range: ITableRange, columnIndex: number, filterOutRows: Set<number>, dateSystem = DateSystem.Date1900) {
|
||||
const filter = this._tableColumnFilterList[columnIndex];
|
||||
if (filter && sheet) {
|
||||
const { startRow, endRow, startColumn } = range;
|
||||
const column = startColumn + columnIndex;
|
||||
const executeFunc = this.getExecuteFunc(sheet, range, columnIndex, filter);
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
if (isColorTableFilter(filter)) {
|
||||
const cellData = sheet.getCell(row, column);
|
||||
const cellStyle = sheet.getComposedCellStyleByCellData(row, column, cellData);
|
||||
if (!executeFunc(cellStyle)) {
|
||||
filterOutRows.add(row);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const conditionType = isConditionFilter(filter) ? filter.filterInfo.conditionType : TableConditionTypeEnum.String;
|
||||
const cellValue = getCellValueWithConditionType(sheet, row, column, conditionType, dateSystem);
|
||||
if (cellValue === null && !executeFunc(cellValue)) {
|
||||
filterOutRows.add(row);
|
||||
} else if (!executeFunc(getCellValueWithConditionType(sheet, row, column, conditionType, dateSystem))) {
|
||||
filterOutRows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getNumberCalculatedOptions(sheet: Worksheet, range: ITableRange, columnIndex: number): ICalculatedOptions {
|
||||
const { startRow, endRow, startColumn } = range;
|
||||
const column = startColumn + columnIndex;
|
||||
const list = [];
|
||||
let count = 0;
|
||||
let sum = 0;
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const val = getCellValueWithConditionType(sheet, row, column, TableConditionTypeEnum.Number) as number;
|
||||
if (val !== null) {
|
||||
list.push(val);
|
||||
count++;
|
||||
sum += val;
|
||||
}
|
||||
}
|
||||
return {
|
||||
list,
|
||||
average: count > 0 ? sum / count : 0,
|
||||
};
|
||||
}
|
||||
|
||||
getExecuteFunc(sheet: Worksheet, range: ITableRange, columnIndex: number, filter: ITableFilterItem): (value: any) => boolean {
|
||||
if (filter.filterType === TableColumnFilterTypeEnum.manual) {
|
||||
const valuesSet = new Set(filter.values);
|
||||
return (value: string) => {
|
||||
if (value == null) {
|
||||
return valuesSet.has(TABLE_FILTER_EMPTY_VALUE);
|
||||
}
|
||||
return valuesSet.has(value);
|
||||
};
|
||||
} else if (filter.filterType === TableColumnFilterTypeEnum.condition) {
|
||||
const isDynamic = isNumberDynamicFilter(filter.filterInfo.compareType);
|
||||
const calculatedOptions = isDynamic ? this._getNumberCalculatedOptions(sheet, range, columnIndex) : undefined;
|
||||
return getConditionExecuteFunc(filter, calculatedOptions);
|
||||
} else if (filter.filterType === TableColumnFilterTypeEnum.color) {
|
||||
const normalizeColor = (color: Nullable<string>) => color ? new ColorKit(color).toRgbString() : null;
|
||||
if (filter.cellFillColors) {
|
||||
const colors = new Set(filter.cellFillColors.map(normalizeColor));
|
||||
return (style: Nullable<IStyleData>) => colors.has(normalizeColor(getColorStyle(style?.bg)));
|
||||
}
|
||||
if (filter.cellTextColors) {
|
||||
const colors = new Set(filter.cellTextColors.map(normalizeColor));
|
||||
return (style: Nullable<IStyleData>) => colors.has(normalizeColor(
|
||||
getColorStyle(style?.cl) ?? getColorStyle(DEFAULT_STYLES.cl)
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return (_value: Nullable<ICellData>) => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
return () => true;
|
||||
}
|
||||
|
||||
toJSON(): ITableFilterJSON {
|
||||
return {
|
||||
tableColumnFilterList: this._tableColumnFilterList,
|
||||
tableSortInfo: this._tableSortInfo,
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json: ITableFilterJSON) {
|
||||
this._tableColumnFilterList = json.tableColumnFilterList ?? [];
|
||||
if (json.tableSortInfo) {
|
||||
this._tableSortInfo = json.tableSortInfo;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._tableColumnFilterList = [];
|
||||
}
|
||||
}
|
||||
514
packages/sheets-table/src/models/table-manager.ts
Normal file
514
packages/sheets-table/src/models/table-manager.ts
Normal file
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* 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, Workbook } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import type {
|
||||
ITableAddedEvent,
|
||||
ITableDeletedEvent,
|
||||
ITableFilterChangedEvent,
|
||||
ITableFilterItem,
|
||||
ITableInfoWithUnitId,
|
||||
ITableJson,
|
||||
ITableNameChangedEvent,
|
||||
ITableOptions,
|
||||
ITableRange,
|
||||
ITableRangeChangedEvent,
|
||||
ITableRangeRowColOperation,
|
||||
ITableRangeUpdate,
|
||||
ITableRangeWithState,
|
||||
ITableResource,
|
||||
ITableSetConfig,
|
||||
ITableThemeChangedEvent,
|
||||
} from '../types/type';
|
||||
import { Disposable, generateRandomId, Inject, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import {
|
||||
IRangeOperationTypeEnum,
|
||||
} from '../types/type';
|
||||
import { convertCellDataToString, getColumnName } from '../util';
|
||||
import { Table } from './table';
|
||||
import { TableColumn } from './table-column';
|
||||
|
||||
export class TableManager extends Disposable {
|
||||
private _tableMap: Map<string, Map<string, Table>>;
|
||||
|
||||
private _tableAdd$ = new Subject<ITableAddedEvent>();
|
||||
public tableAdd$ = this._tableAdd$.asObservable();
|
||||
|
||||
private _tableDelete$ = new Subject<ITableDeletedEvent>();
|
||||
public tableDelete$ = this._tableDelete$.asObservable();
|
||||
|
||||
private _tableNameChanged$ = new Subject<ITableNameChangedEvent>();
|
||||
public tableNameChanged$ = this._tableNameChanged$.asObservable();
|
||||
|
||||
private _tableRangeChanged$ = new Subject<ITableRangeChangedEvent>();
|
||||
public tableRangeChanged$ = this._tableRangeChanged$.asObservable();
|
||||
|
||||
private _tableThemeChanged$ = new Subject<ITableThemeChangedEvent>();
|
||||
public tableThemeChanged$ = this._tableThemeChanged$.asObservable();
|
||||
|
||||
private _tableFilterChanged$ = new Subject<ITableFilterChangedEvent>();
|
||||
public tableFilterChanged$ = this._tableFilterChanged$.asObservable();
|
||||
|
||||
private _tableInitStatus = new BehaviorSubject<boolean>(false);
|
||||
public tableInitStatus$ = this._tableInitStatus.asObservable();
|
||||
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService
|
||||
) {
|
||||
super();
|
||||
this._tableMap = new Map();
|
||||
}
|
||||
|
||||
private _ensureUnit(unitId: string) {
|
||||
if (!this._tableMap.has(unitId)) {
|
||||
this._tableMap.set(unitId, new Map());
|
||||
}
|
||||
return this._tableMap.get(unitId)!;
|
||||
}
|
||||
|
||||
getColumnHeader(unitId: string, subUnitId: string, range: ITableRange, prefixText?: string) {
|
||||
const worksheet = this._univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetId(subUnitId);
|
||||
|
||||
const { startRow, startColumn, endColumn } = range;
|
||||
const header = [];
|
||||
const columnText = prefixText ?? 'Column';
|
||||
for (let i = startColumn; i <= endColumn; i++) {
|
||||
header.push(convertCellDataToString(worksheet?.getCell(startRow, i)) || getColumnName(i - startColumn + 1, columnText));
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a table to univer.
|
||||
* @param {string} unitId The unit id of the table.
|
||||
* @param {string} subUnitId The subunit id of the table.
|
||||
* @param {string} name The table name, it should be unique in the unit or it will be appended with a number.
|
||||
* @param {ITableRange} range The range of the table, it contains the unit id and subunit id.
|
||||
* @param {string[]} [header] The header of the table, if not provided, it will be generated based on the range.
|
||||
* @param {string} [initId] The initial id of the table, if not provided, a random id will be generated.
|
||||
* @param {ITableOptions} [options] Other options of the table.
|
||||
* @returns {string} The table id.
|
||||
*/
|
||||
addTable(unitId: string, subUnitId: string, name: string, range: ITableRange, header?: string[], initId?: string, options?: ITableOptions): string {
|
||||
const id = initId ?? generateRandomId();
|
||||
const tableHeader = header || this.getColumnHeader(unitId, subUnitId, range);
|
||||
const table = new Table(id, name, range, tableHeader, options);
|
||||
// set subunit id to table
|
||||
table.setSubunitId(subUnitId);
|
||||
const unitMap = this._ensureUnit(unitId);
|
||||
unitMap.set(id, table);
|
||||
this._tableAdd$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
range,
|
||||
tableName: name,
|
||||
tableId: id,
|
||||
tableStyleId: options?.tableStyleId,
|
||||
});
|
||||
|
||||
if (options?.filters) {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
table.getTableFilters().doFilter(worksheet!, range, workbook?.getDateSystem());
|
||||
this._tableFilterChanged$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId: id,
|
||||
});
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
addFilter(unitId: string, tableId: string, column: number, filter: ITableFilterItem | undefined) {
|
||||
const table = this.getTable(unitId, tableId);
|
||||
if (table) {
|
||||
const tableFilter = table.getTableFilters();
|
||||
tableFilter.setColumnFilter(column, filter);
|
||||
const subUnitId = table.getSubunitId();
|
||||
this._tableFilterChanged$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getFilterRanges(unitId: string, subUnitId: string): IRange[] {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return [];
|
||||
}
|
||||
const filterRanges: IRange[] = [];
|
||||
unitMap.forEach((table) => {
|
||||
if (table.getSubunitId() === subUnitId && table.isShowHeader()) {
|
||||
filterRanges.push(table.getRange());
|
||||
}
|
||||
});
|
||||
return filterRanges;
|
||||
}
|
||||
|
||||
getSheetFilterRangeWithState(unitId: string, subUnitId: string): ITableRangeWithState[] {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return [];
|
||||
}
|
||||
const filterRanges: ITableRangeWithState[] = [];
|
||||
unitMap.forEach((table) => {
|
||||
if (table.getSubunitId() === subUnitId && table.isShowHeader()) {
|
||||
filterRanges.push({
|
||||
tableId: table.getId(),
|
||||
range: table.getRange(),
|
||||
states: table.getFilterStates(table.getRange()),
|
||||
});
|
||||
}
|
||||
});
|
||||
return filterRanges;
|
||||
}
|
||||
|
||||
getTable(unitId: string, tableId: string) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return undefined;
|
||||
}
|
||||
return unitMap.get(tableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique table name, in excel, the table name should be unique because it is used as a reference.
|
||||
* @param {string} unitId The unit id of the table.
|
||||
* @param {string} baseName The base name of the table.
|
||||
* @returns {string} The unique table name
|
||||
*/
|
||||
getUniqueTableName(unitId: string, baseName: string) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return baseName;
|
||||
}
|
||||
const tableNamesSet = new Set(Array.from(unitMap.values()).map((table) => table.getDisplayName()));
|
||||
let newName = baseName;
|
||||
let count = 1;
|
||||
while (tableNamesSet.has(newName)) {
|
||||
newName = `${baseName}-${count}`;
|
||||
count++;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table by unit id and table id.
|
||||
* @param {string} unitId The unit id of the table.
|
||||
* @param {string} tableId The table id.
|
||||
* @returns {Table} The table.
|
||||
*/
|
||||
getTableById(unitId: string, tableId: string) {
|
||||
return this.getTable(unitId, tableId);
|
||||
}
|
||||
|
||||
getTableList(unitId: string): ITableInfoWithUnitId[] {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return [] as ITableInfoWithUnitId[];
|
||||
}
|
||||
return Array.from(unitMap.values()).map((table) => {
|
||||
return { ...table.getTableInfo(), unitId };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table list by unit id and subunit id.
|
||||
* @param {string} unitId The unit id of the table.
|
||||
* @param {string} subUnitId The subunit id of the table.
|
||||
* @returns {Table[]} The table list.
|
||||
*/
|
||||
getTablesBySubunitId(unitId: string, subUnitId: string) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (!unitMap) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(unitMap.values()).filter((table) => table.getSubunitId() === subUnitId);
|
||||
}
|
||||
|
||||
getTablesInfoBySubunitId(unitId: string, subUnitId: string) {
|
||||
return this.getTablesBySubunitId(unitId, subUnitId).map((table) => {
|
||||
return {
|
||||
id: table.getId(),
|
||||
name: table.getDisplayName(),
|
||||
range: table.getRange(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
deleteTable(unitId: string, tableId: string) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
const table = unitMap?.get(tableId);
|
||||
if (table) {
|
||||
const tableInfo = table.getTableInfo();
|
||||
const tableStyleId = table.getTableStyleId();
|
||||
unitMap?.delete(tableId);
|
||||
const { subUnitId, range, name } = tableInfo;
|
||||
this._tableDelete$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
range,
|
||||
tableName: name,
|
||||
tableStyleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
operationTableRowCol(unitId: string, tableId: string, config: ITableRangeRowColOperation) {
|
||||
const table = this.getTableById(unitId, tableId);
|
||||
if (!table) return;
|
||||
const { operationType, rowColType, index, count, columnsJson } = config;
|
||||
const oldRange = table.getRange();
|
||||
const newRange = { ...oldRange };
|
||||
if (operationType === IRangeOperationTypeEnum.Insert) {
|
||||
if (rowColType === 'row') {
|
||||
newRange.endRow += count;
|
||||
} else if (rowColType === 'column') {
|
||||
newRange.endColumn += count;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// There is no need to enforce column name uniqueness.
|
||||
const columnPrefix = this._localeService.t<LocaleKey>('sheets-table.columnPrefix');
|
||||
const column = new TableColumn(generateRandomId(), getColumnName(table.getColumnsCount() + 1 + i, columnPrefix));
|
||||
if (columnsJson?.[i]) {
|
||||
column.fromJSON(columnsJson[i]);
|
||||
}
|
||||
const columnIndex = index + i - oldRange.startColumn;
|
||||
table.insertColumn(columnIndex, column);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (rowColType === 'row') {
|
||||
newRange.endRow -= count;
|
||||
} else if (rowColType === 'column') {
|
||||
newRange.endColumn -= count;
|
||||
// Remove columns from the end to avoid index shifting
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
const columnIndex = index + i - oldRange.startColumn;
|
||||
table.removeColumn(columnIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.setRange(newRange);
|
||||
this._tableRangeChanged$.next({
|
||||
unitId,
|
||||
subUnitId: table.getSubunitId(),
|
||||
tableId,
|
||||
range: newRange,
|
||||
oldRange,
|
||||
});
|
||||
}
|
||||
|
||||
updateTableRange(unitId: string, tableId: string, config: ITableRangeUpdate) {
|
||||
const table = this.getTableById(unitId, tableId);
|
||||
if (!table) return;
|
||||
const oldRange = table.getRange();
|
||||
const newRange = config.newRange;
|
||||
if (newRange.startColumn < oldRange.startColumn) {
|
||||
const diff = oldRange.startColumn - newRange.startColumn;
|
||||
const columnPrefix = this._localeService.t<LocaleKey>('sheets-table.columnPrefix');
|
||||
for (let i = 0; i < diff; i++) {
|
||||
table.insertColumn(oldRange.startColumn, new TableColumn(generateRandomId(), getColumnName(table.getColumnsCount() + 1, columnPrefix)));
|
||||
}
|
||||
} else if (newRange.startColumn > oldRange.startColumn) {
|
||||
const diff = newRange.startColumn - oldRange.startColumn;
|
||||
for (let i = diff - 1; i >= 0; i--) {
|
||||
const columnIndex = newRange.startColumn + i - oldRange.startColumn;
|
||||
table.removeColumn(columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (newRange.endColumn < oldRange.endColumn) {
|
||||
const diff = oldRange.endColumn - newRange.endColumn;
|
||||
for (let i = diff - 1; i >= 0; i--) {
|
||||
const columnIndex = newRange.endColumn + i - oldRange.startColumn;
|
||||
table.removeColumn(columnIndex);
|
||||
}
|
||||
} else if (newRange.endColumn > oldRange.endColumn) {
|
||||
const diff = newRange.endColumn - oldRange.endColumn;
|
||||
const columnPrefix = this._localeService.t<LocaleKey>('sheets-table.columnPrefix');
|
||||
for (let i = 0; i < diff; i++) {
|
||||
table.insertColumn(oldRange.endColumn, new TableColumn(generateRandomId(), getColumnName(table.getColumnsCount() + 1, columnPrefix)));
|
||||
}
|
||||
}
|
||||
|
||||
table.setRange(newRange);
|
||||
this._tableRangeChanged$.next({
|
||||
unitId,
|
||||
subUnitId: table.getSubunitId(),
|
||||
tableId,
|
||||
range: newRange,
|
||||
oldRange,
|
||||
});
|
||||
}
|
||||
|
||||
setTableByConfig(unitId: string, tableId: string, config: ITableSetConfig) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
const table = unitMap?.get(tableId);
|
||||
if (!table) return;
|
||||
const subUnitId = table.getSubunitId();
|
||||
const { name, updateRange, rowColOperation, theme, sortInfo, options } = config;
|
||||
if (name) {
|
||||
const oldTableName = table.getDisplayName();
|
||||
table.setDisplayName(name);
|
||||
this._tableNameChanged$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
tableName: name,
|
||||
oldTableName,
|
||||
});
|
||||
}
|
||||
if (rowColOperation) {
|
||||
this.operationTableRowCol(unitId, tableId, rowColOperation);
|
||||
}
|
||||
if (updateRange) {
|
||||
this.updateTableRange(unitId, tableId, updateRange);
|
||||
}
|
||||
if (theme) {
|
||||
const oldTheme = table.getTableStyleId() ?? 'default';
|
||||
table.setTableStyleId(theme);
|
||||
this._tableThemeChanged$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
tableId,
|
||||
theme,
|
||||
oldTheme,
|
||||
});
|
||||
}
|
||||
|
||||
if ('sortInfo' in config) {
|
||||
table.getTableFilters().setSortInfo(sortInfo);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
if (options.showHeader !== undefined) {
|
||||
table.setShowHeader(options.showHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(unitId: string): ITableResource {
|
||||
const result: ITableResource = {};
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (unitMap) {
|
||||
unitMap.forEach((table) => {
|
||||
const subUnitId = table.getSubunitId();
|
||||
if (!result[subUnitId]) {
|
||||
const tableFilteredOutRows = new Set<number>();
|
||||
const tables = this.getTablesBySubunitId(unitId, subUnitId);
|
||||
tables.forEach((table) => {
|
||||
const tableFilteredRows = table.getTableFilters().getFilterOutRows();
|
||||
if (!tableFilteredRows) {
|
||||
return;
|
||||
}
|
||||
for (const row of tableFilteredRows) {
|
||||
tableFilteredOutRows.add(row);
|
||||
}
|
||||
});
|
||||
result[subUnitId] = {
|
||||
tables: [] as ITableJson[],
|
||||
tableFilteredOutRows: Array.from(tableFilteredOutRows),
|
||||
};
|
||||
}
|
||||
result[subUnitId].tables.push(table.toJSON());
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fromJSON(unitId: string, data: ITableResource) {
|
||||
const unitMap = this._ensureUnit(unitId);
|
||||
const subUnitIds = Object.keys(data);
|
||||
subUnitIds.forEach((subUnitId) => {
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, { unitId, subUnitId });
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const sheet = target.worksheet;
|
||||
let tables;
|
||||
// adjust code for older data structures
|
||||
if (data[subUnitId].tables) {
|
||||
tables = data[subUnitId].tables;
|
||||
} else if (Array.isArray(data[subUnitId])) {
|
||||
// for older versions, where tables were not nested under subUnitId
|
||||
tables = data[subUnitId] as any as ITableJson[];
|
||||
}
|
||||
if (!tables) {
|
||||
return;
|
||||
}
|
||||
tables.forEach((table) => {
|
||||
const header = this.getColumnHeader(unitId, subUnitId, table.range);
|
||||
const tableInstance = new Table(table.id, table.name, table.range, header, table.options);
|
||||
tableInstance.setTableMeta(table.meta);
|
||||
if (table.columns.length) {
|
||||
tableInstance.setColumns(table.columns);
|
||||
}
|
||||
if (table.filters) {
|
||||
const tableFilter = tableInstance.getTableFilters();
|
||||
tableFilter.fromJSON(table.filters);
|
||||
tableFilter.doFilter(sheet, tableInstance.getTableFilterRange(), target.workbook.getDateSystem());
|
||||
}
|
||||
tableInstance.setSubunitId(subUnitId);
|
||||
unitMap.set(table.id, tableInstance);
|
||||
this._tableAdd$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: table.range,
|
||||
tableName: table.name,
|
||||
tableId: table.id,
|
||||
tableStyleId: table.options?.tableStyleId,
|
||||
});
|
||||
});
|
||||
});
|
||||
this._tableInitStatus.next(true);
|
||||
}
|
||||
|
||||
deleteUnitId(unitId: string) {
|
||||
const unitMap = this._tableMap.get(unitId);
|
||||
if (unitMap) {
|
||||
unitMap.forEach((table) => table.dispose());
|
||||
}
|
||||
this._tableMap.delete(unitId);
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
|
||||
this._tableAdd$.complete();
|
||||
this._tableDelete$.complete();
|
||||
this._tableNameChanged$.complete();
|
||||
this._tableRangeChanged$.complete();
|
||||
this._tableThemeChanged$.complete();
|
||||
this._tableFilterChanged$.complete();
|
||||
this._tableInitStatus.complete();
|
||||
|
||||
this._tableMap.forEach((unitMap) => {
|
||||
unitMap.forEach((table) => table.dispose());
|
||||
unitMap.clear();
|
||||
});
|
||||
this._tableMap.clear();
|
||||
}
|
||||
}
|
||||
298
packages/sheets-table/src/models/table.ts
Normal file
298
packages/sheets-table/src/models/table.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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 { ITableColumnJson, ITableFilterItem, ITableInfo, ITableJson, ITableOptions, ITableRange } from '../types/type';
|
||||
import { generateRandomId } from '@univerjs/core';
|
||||
import { tableThemeConfig } from '../controllers/table-theme.factory';
|
||||
import { TableColumn } from './table-column';
|
||||
import { TableFilters } from './table-filter';
|
||||
|
||||
export class Table {
|
||||
private _id: string;
|
||||
private _name: string;
|
||||
/**
|
||||
* @property {string} tableStyleId The table style id. If the property is empty, the default style will be used.
|
||||
*/
|
||||
private _tableStyleId?: string;
|
||||
private _showHeader: boolean;
|
||||
private _showFooter: boolean;
|
||||
private _range: ITableRange;
|
||||
private _columns: Map<string, TableColumn> = new Map();
|
||||
private _columnOrder: string[] = [];
|
||||
tableMeta: Record<string, any>;
|
||||
private _tableFilters: TableFilters;
|
||||
|
||||
private _subUnitId: string;
|
||||
|
||||
constructor(id: string, name: string, range: ITableRange, header: string[], options: ITableOptions = {}) {
|
||||
this._id = id;
|
||||
this._range = range;
|
||||
this._name = name;
|
||||
this._tableFilters = new TableFilters();
|
||||
this._init(header, options);
|
||||
}
|
||||
|
||||
_init(header: string[], options: ITableOptions) {
|
||||
this._tableStyleId = options?.tableStyleId;
|
||||
this._showHeader = options?.showHeader ?? true;
|
||||
// Summarization is not supported yet. Footer is not displayed yet.
|
||||
this._showFooter = false;
|
||||
// init table columns
|
||||
const range = this.getRange();
|
||||
const startColumn = range.startColumn;
|
||||
const endColumn = range.endColumn;
|
||||
|
||||
for (let i = startColumn; i <= endColumn; i++) {
|
||||
const index = i - startColumn;
|
||||
|
||||
let id;
|
||||
let columnName;
|
||||
|
||||
if (options.columns?.[index]) {
|
||||
id = options.columns[index].id;
|
||||
columnName = options.columns[index].displayName;
|
||||
} else {
|
||||
id = generateRandomId();
|
||||
columnName = header[i - startColumn];
|
||||
}
|
||||
|
||||
const column = new TableColumn(id, columnName);
|
||||
this._columns.set(id, column);
|
||||
this._columnOrder.push(id);
|
||||
}
|
||||
|
||||
// this._tableFilters = new TableFilters();
|
||||
|
||||
if (options.filters) {
|
||||
const filters = options.filters;
|
||||
filters.forEach((filter, index) => {
|
||||
if (filter) {
|
||||
this._tableFilters.setColumnFilter(index, filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTableFilterColumn(columnIndex: number, filter: ITableFilterItem) {
|
||||
this._tableFilters.setColumnFilter(columnIndex, filter);
|
||||
}
|
||||
|
||||
getTableFilterColumn(columnIndex: number) {
|
||||
return this._tableFilters.getColumnFilter(columnIndex);
|
||||
}
|
||||
|
||||
getTableFilters() {
|
||||
return this._tableFilters;
|
||||
}
|
||||
|
||||
getTableFilterRange() {
|
||||
const tableRange = this.getRange();
|
||||
const showHeader = this.isShowHeader();
|
||||
const isShowFooter = this.isShowFooter();
|
||||
const { startRow, startColumn, endRow, endColumn } = tableRange;
|
||||
const start = showHeader ? startRow + 1 : startRow;
|
||||
const end = isShowFooter ? endRow - 1 : endRow;
|
||||
return {
|
||||
startRow: start,
|
||||
startColumn,
|
||||
endRow: end,
|
||||
endColumn,
|
||||
};
|
||||
}
|
||||
|
||||
setColumns(columns: ITableColumnJson[]) {
|
||||
this._columns.clear();
|
||||
this._columnOrder = [];
|
||||
columns.forEach((columnJson) => {
|
||||
const column = new TableColumn(columnJson.id, columnJson.displayName);
|
||||
column.fromJSON(columnJson);
|
||||
this._columns.set(columnJson.id, column);
|
||||
this._columnOrder.push(columnJson.id);
|
||||
});
|
||||
}
|
||||
|
||||
getColumnsCount() {
|
||||
return this._columnOrder.length;
|
||||
}
|
||||
|
||||
insertColumn(index: number, column: TableColumn) {
|
||||
const columnId = column.id;
|
||||
this._columns.set(columnId, column);
|
||||
this._columnOrder.splice(index, 0, columnId);
|
||||
}
|
||||
|
||||
removeColumn(index: number) {
|
||||
const columnId = this._columnOrder[index];
|
||||
this._columns.delete(columnId);
|
||||
this._columnOrder.splice(index, 1);
|
||||
}
|
||||
|
||||
setTableMeta(meta: Record<string, any>) {
|
||||
this.tableMeta = meta;
|
||||
}
|
||||
|
||||
getTableMeta() {
|
||||
return this.tableMeta;
|
||||
}
|
||||
|
||||
getColumn(columnId: string) {
|
||||
return this._columns.get(columnId);
|
||||
}
|
||||
|
||||
getTableColumnByIndex(index: number) {
|
||||
const id = this._columnOrder[index];
|
||||
return this.getColumn(id);
|
||||
}
|
||||
|
||||
getColumnNameByIndex(index: number) {
|
||||
const id = this._columnOrder[index];
|
||||
return this.getColumn(id)?.getDisplayName() || '';
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
getRangeInfo() {
|
||||
return {
|
||||
...this._range,
|
||||
};
|
||||
}
|
||||
|
||||
getRange() {
|
||||
return { ...this._range };
|
||||
}
|
||||
|
||||
setRange(range: ITableRange) {
|
||||
this._range = range;
|
||||
}
|
||||
|
||||
setDisplayName(name: string) {
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
getSubunitId() {
|
||||
return this._subUnitId;
|
||||
}
|
||||
|
||||
setSubunitId(subUnitId: string) {
|
||||
this._subUnitId = subUnitId;
|
||||
}
|
||||
|
||||
getTableStyleId() {
|
||||
return this._tableStyleId ?? tableThemeConfig[0].name;
|
||||
}
|
||||
|
||||
setTableStyleId(tableStyleId: string) {
|
||||
this._tableStyleId = tableStyleId;
|
||||
}
|
||||
|
||||
isShowHeader() {
|
||||
return this._showHeader ?? true;
|
||||
}
|
||||
|
||||
setShowHeader(showHeader: boolean) {
|
||||
this._showHeader = showHeader;
|
||||
}
|
||||
|
||||
isShowFooter() {
|
||||
return this._showFooter ?? false;
|
||||
}
|
||||
|
||||
getTableInfo(): ITableInfo {
|
||||
return {
|
||||
id: this._id,
|
||||
subUnitId: this._subUnitId,
|
||||
name: this._name,
|
||||
range: this.getRangeInfo(),
|
||||
meta: this.tableMeta,
|
||||
showHeader: this._showHeader,
|
||||
columns: this._columnOrder.map((columnId) => this._columns.get(columnId)!.toJSON()),
|
||||
};
|
||||
}
|
||||
|
||||
getTableConfig() {
|
||||
return {
|
||||
name: this.getDisplayName(),
|
||||
range: this.getRangeInfo(),
|
||||
options: {
|
||||
showHeader: this._showHeader,
|
||||
showFooter: this._showFooter,
|
||||
},
|
||||
tableStyleId: this._tableStyleId,
|
||||
};
|
||||
}
|
||||
|
||||
getFilterStates(range: ITableRange) {
|
||||
return this._tableFilters.getFilterStates(range);
|
||||
}
|
||||
|
||||
toJSON(): ITableJson {
|
||||
const columns: ITableColumnJson[] = [];
|
||||
this._columns.forEach((column) => {
|
||||
columns.push(column.toJSON());
|
||||
});
|
||||
return {
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
range: this.getRangeInfo(),
|
||||
options: {
|
||||
showHeader: this._showHeader,
|
||||
showFooter: this._showFooter,
|
||||
tableStyleId: this._tableStyleId,
|
||||
},
|
||||
// TODO: support filter
|
||||
filters: this._tableFilters.toJSON(),
|
||||
columns,
|
||||
meta: this.tableMeta,
|
||||
};
|
||||
}
|
||||
|
||||
fromJSON(json: ITableJson) {
|
||||
this._id = json.id;
|
||||
this._name = json.name;
|
||||
this._range = json.range;
|
||||
this.tableMeta = json.meta;
|
||||
this._tableStyleId = json.options.tableStyleId || '';
|
||||
this._showHeader = json.options.showHeader ?? true;
|
||||
this._showFooter = json.options.showFooter ?? true;
|
||||
const columns = json.columns;
|
||||
columns.forEach((column) => {
|
||||
const tableColumn = new TableColumn(column.id, column.displayName);
|
||||
tableColumn.fromJSON(column);
|
||||
this._columns.set(column.id, tableColumn);
|
||||
this._columnOrder.push(column.id);
|
||||
});
|
||||
this._tableFilters = new TableFilters();
|
||||
this._tableFilters.fromJSON(json.filters);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._id = '';
|
||||
this._name = '';
|
||||
this._tableStyleId = '';
|
||||
this._showHeader = true;
|
||||
this._showFooter = true;
|
||||
// @ts-ignore
|
||||
delete this._range;
|
||||
this._columns.clear();
|
||||
this._columnOrder = [];
|
||||
}
|
||||
}
|
||||
120
packages/sheets-table/src/plugin.ts
Normal file
120
packages/sheets-table/src/plugin.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IUniverSheetsTableConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import pkg from '../package.json';
|
||||
import { AddSheetTableCommand } from './commands/commands/add-sheet-table.command';
|
||||
import { AddTableThemeCommand } from './commands/commands/add-table-theme.command';
|
||||
import { DeleteSheetTableCommand } from './commands/commands/delete-sheet-table.command';
|
||||
import { RemoveTableThemeCommand } from './commands/commands/remove-table-theme.command';
|
||||
import { SetSheetTableCommand } from './commands/commands/set-sheet-table.command';
|
||||
import { SetSheetTableFilterCommand } from './commands/commands/set-table-filter.command';
|
||||
import { SetSheetTableSortStateCommand } from './commands/commands/set-table-sort-state.command';
|
||||
import { SheetTableInsertColCommand, SheetTableInsertColumnAtCommand, SheetTableInsertRowAtCommand, SheetTableInsertRowCommand, SheetTableRemoveColCommand, SheetTableRemoveColumnAtCommand, SheetTableRemoveRowCommand } from './commands/commands/sheet-table-row-col.command';
|
||||
import { AddSheetTableMutation } from './commands/mutations/add-sheet-table.mutation';
|
||||
import { DeleteSheetTableMutation } from './commands/mutations/delete-sheet-table.mutation';
|
||||
import { SetSheetTableMutation } from './commands/mutations/set-sheet-table.mutation';
|
||||
import { SetSheetTableFilterMutation } from './commands/mutations/set-table-filter.mutation';
|
||||
import { defaultPluginConfig, SHEETS_TABLE_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { PLUGIN_NAME } from './const';
|
||||
import { SheetTableFormulaController } from './controllers/sheet-table-formula.controller';
|
||||
import { SheetTableRangeController } from './controllers/sheet-table-range.controller';
|
||||
import { SheetTableRefRangeController } from './controllers/sheet-table-ref-range.controller';
|
||||
import { SheetsTableThemeController } from './controllers/sheet-table-theme.controller';
|
||||
import { SheetsTableController } from './controllers/sheets-table.controller';
|
||||
import { TableFilterController } from './controllers/table-filter.controller';
|
||||
import { TableManager } from './models/table-manager';
|
||||
import { SheetTableService } from './services/table.service';
|
||||
|
||||
@DependentOn(UniverSheetsPlugin)
|
||||
export class UniverSheetsTablePlugin extends Plugin {
|
||||
static override pluginName = PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsTableConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_TABLE_PLUGIN_CONFIG_KEY, rest);
|
||||
|
||||
this._initRegisterCommand();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
registerDependencies(this._injector, [
|
||||
[TableManager],
|
||||
[SheetsTableThemeController],
|
||||
[SheetsTableController],
|
||||
[SheetTableService],
|
||||
[TableFilterController],
|
||||
[SheetTableRangeController],
|
||||
[SheetTableRefRangeController],
|
||||
[SheetTableFormulaController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetTableFormulaController],
|
||||
[SheetTableRangeController],
|
||||
[SheetTableRefRangeController],
|
||||
[SheetsTableThemeController],
|
||||
[SheetsTableController],
|
||||
[SheetTableService],
|
||||
[TableFilterController],
|
||||
]);
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[TableManager],
|
||||
]);
|
||||
}
|
||||
|
||||
private _initRegisterCommand() {
|
||||
[
|
||||
AddSheetTableCommand,
|
||||
AddSheetTableMutation,
|
||||
DeleteSheetTableCommand,
|
||||
DeleteSheetTableMutation,
|
||||
SetSheetTableFilterMutation,
|
||||
SetSheetTableFilterCommand,
|
||||
SetSheetTableSortStateCommand,
|
||||
SetSheetTableCommand,
|
||||
SetSheetTableMutation,
|
||||
AddTableThemeCommand,
|
||||
RemoveTableThemeCommand,
|
||||
SheetTableInsertRowCommand,
|
||||
SheetTableInsertColCommand,
|
||||
SheetTableInsertRowAtCommand,
|
||||
SheetTableInsertColumnAtCommand,
|
||||
SheetTableRemoveRowCommand,
|
||||
SheetTableRemoveColCommand,
|
||||
SheetTableRemoveColumnAtCommand,
|
||||
].forEach((m) => this._commandService.registerCommand(m));
|
||||
}
|
||||
}
|
||||
@@ -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 { Injector } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as conditionUtil from '../../models/filter-util/condition';
|
||||
import { TableManager } from '../../models/table-manager';
|
||||
import { TableConditionTypeEnum } from '../../types/enum';
|
||||
import { SheetTableService } from '../table.service';
|
||||
|
||||
function createService(tableManager: Record<string, unknown>) {
|
||||
class TestTableManager {
|
||||
getTable = tableManager.getTable;
|
||||
getTableList = tableManager.getTableList;
|
||||
addTable = tableManager.addTable;
|
||||
deleteTable = tableManager.deleteTable;
|
||||
addFilter = tableManager.addFilter;
|
||||
}
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([TableManager, { useClass: TestTableManager }]);
|
||||
injector.add([SheetTableService]);
|
||||
return injector.get(SheetTableService);
|
||||
}
|
||||
|
||||
describe('SheetTableService', () => {
|
||||
it('should proxy table CRUD and metadata calls to table manager', () => {
|
||||
const setMeta = vi.fn();
|
||||
const setColumnMeta = vi.fn();
|
||||
const column = {
|
||||
getMeta: vi.fn(() => ({ key: 'column-meta' })),
|
||||
setMeta: setColumnMeta,
|
||||
};
|
||||
const table = {
|
||||
getTableInfo: vi.fn(() => ({ id: 'table-1', name: 'Table 1' })),
|
||||
getTableMeta: vi.fn(() => ({ key: 'table-meta' })),
|
||||
setTableMeta: setMeta,
|
||||
getTableColumnByIndex: vi.fn(() => column),
|
||||
};
|
||||
|
||||
const tableManager = {
|
||||
getTable: vi.fn(() => table),
|
||||
getTableList: vi.fn(() => [{ id: 'table-1' }]),
|
||||
addTable: vi.fn(() => 'table-1'),
|
||||
deleteTable: vi.fn(),
|
||||
addFilter: vi.fn(),
|
||||
};
|
||||
|
||||
const service = createService(tableManager);
|
||||
|
||||
expect(service.getTableInfo('u1', 'table-1')).toEqual({
|
||||
unitId: 'u1',
|
||||
id: 'table-1',
|
||||
name: 'Table 1',
|
||||
});
|
||||
expect(service.getTableList('u1')).toEqual([{ id: 'table-1' }]);
|
||||
expect(service.addTable('u1', 's1', 'Table 1', { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 } as any)).toBe('table-1');
|
||||
|
||||
service.deleteTable('u1', 's1', 'table-1');
|
||||
expect(tableManager.deleteTable).toHaveBeenCalledWith('u1', 'table-1');
|
||||
|
||||
expect(service.getTableMeta('u1', 'table-1')).toEqual({ key: 'table-meta' });
|
||||
service.setTableMeta('u1', 'table-1', { updated: true } as any);
|
||||
expect(setMeta).toHaveBeenCalledWith({ updated: true });
|
||||
|
||||
expect(service.getTableColumnMeta('u1', 'table-1', 0)).toEqual({ key: 'column-meta' });
|
||||
service.selTableColumnMeta('u1', 'table-1', 0, { updated: true } as any);
|
||||
expect(setColumnMeta).toHaveBeenCalledWith({ updated: true });
|
||||
|
||||
service.addFilter('u1', 'table-1', 1, { filterType: 'manual', values: ['A'] } as any);
|
||||
expect(tableManager.addFilter).toHaveBeenCalledWith('u1', 'table-1', 1, { filterType: 'manual', values: ['A'] });
|
||||
});
|
||||
|
||||
it('should return undefined when table does not exist', () => {
|
||||
const service = createService({ getTable: () => undefined });
|
||||
expect(service.getTableInfo('u1', 'missing')).toBeUndefined();
|
||||
expect(service.getTableMeta('u1', 'missing')).toBeUndefined();
|
||||
expect(service.getTableColumnMeta('u1', 'missing', 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delegate cell value extraction by condition type', () => {
|
||||
const spy = vi.spyOn(conditionUtil, 'getCellValueWithConditionType').mockReturnValue('ok' as any);
|
||||
const service = createService({});
|
||||
|
||||
const worksheet = { id: 'sheet' } as any;
|
||||
const value = service.getCellValueWithConditionType(worksheet, 2, 3, TableConditionTypeEnum.Number);
|
||||
|
||||
expect(value).toBe('ok');
|
||||
expect(spy).toHaveBeenCalledWith(worksheet, 2, 3, TableConditionTypeEnum.Number);
|
||||
});
|
||||
});
|
||||
77
packages/sheets-table/src/services/table.service.ts
Normal file
77
packages/sheets-table/src/services/table.service.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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';
|
||||
import type { ITableFilterItem, ITableInfoWithUnitId, ITableOptions, ITableRange, TableMetaType } from '../types/type';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { getCellValueWithConditionType } from '../models/filter-util/condition';
|
||||
import { TableManager } from '../models/table-manager';
|
||||
import { TableConditionTypeEnum } from '../types/enum';
|
||||
|
||||
export class SheetTableService extends Disposable {
|
||||
constructor(
|
||||
@Inject(TableManager) private _tableManager: TableManager
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTableInfo(unitId: string, tableId: string): ITableInfoWithUnitId | undefined {
|
||||
const table = this._tableManager.getTable(unitId, tableId);
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
unitId,
|
||||
...table.getTableInfo(),
|
||||
};
|
||||
}
|
||||
|
||||
getTableList(unitId: string): ITableInfoWithUnitId[] {
|
||||
return this._tableManager.getTableList(unitId);
|
||||
}
|
||||
|
||||
addTable(unitId: string, subUnitId: string, tableName: string, rangeInfo: ITableRange, tableHeader?: string[], tableId?: string, options?: ITableOptions): string {
|
||||
return this._tableManager.addTable(unitId, subUnitId, tableName, rangeInfo, tableHeader, tableId, options);
|
||||
}
|
||||
|
||||
deleteTable(unitId: string, subUnitId: string, tableId: string) {
|
||||
this._tableManager.deleteTable(unitId, tableId);
|
||||
}
|
||||
|
||||
getTableMeta(unitId: string, tableId: string): TableMetaType | undefined {
|
||||
return this._tableManager.getTable(unitId, tableId)?.getTableMeta();
|
||||
}
|
||||
|
||||
setTableMeta(unitId: string, tableId: string, meta: TableMetaType) {
|
||||
this._tableManager.getTable(unitId, tableId)?.setTableMeta(meta);
|
||||
}
|
||||
|
||||
getTableColumnMeta(unitId: string, tableId: string, index: number): TableMetaType | undefined {
|
||||
return this._tableManager.getTable(unitId, tableId)?.getTableColumnByIndex(index)?.getMeta();
|
||||
}
|
||||
|
||||
selTableColumnMeta(unitId: string, tableId: string, index: number, meta: TableMetaType) {
|
||||
this._tableManager.getTable(unitId, tableId)?.getTableColumnByIndex(index)?.setMeta(meta);
|
||||
}
|
||||
|
||||
addFilter(unitId: string, tableId: string, column: number, filter: ITableFilterItem) {
|
||||
this._tableManager.addFilter(unitId, tableId, column, filter);
|
||||
}
|
||||
|
||||
getCellValueWithConditionType(sheet: Worksheet, row: number, col: number, conditionType: TableConditionTypeEnum = TableConditionTypeEnum.String) {
|
||||
return getCellValueWithConditionType(sheet, row, col, conditionType);
|
||||
}
|
||||
}
|
||||
129
packages/sheets-table/src/types/enum.ts
Normal file
129
packages/sheets-table/src/types/enum.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 enum TableColumnDataTypeEnum {
|
||||
None = 'none',
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Date = 'date',
|
||||
Bool = 'bool',
|
||||
Checkbox = 'checkbox',
|
||||
List = 'list',
|
||||
};
|
||||
|
||||
export enum TableColumnFilterTypeEnum {
|
||||
manual = 'manual',
|
||||
condition = 'condition',
|
||||
color = 'color',
|
||||
}
|
||||
|
||||
export enum TableConditionTypeEnum {
|
||||
Date = 'date',
|
||||
Number = 'number',
|
||||
String = 'string',
|
||||
Logic = 'logic',
|
||||
}
|
||||
|
||||
export enum TableNumberCompareTypeEnum {
|
||||
Equal = 'equal',
|
||||
NotEqual = 'notEqual',
|
||||
GreaterThan = 'greaterThan',
|
||||
GreaterThanOrEqual = 'greaterThanOrEqual',
|
||||
LessThan = 'lessThan',
|
||||
LessThanOrEqual = 'lessThanOrEqual',
|
||||
Between = 'between',
|
||||
NotBetween = 'notBetween',
|
||||
Above = 'above',
|
||||
Below = 'below',
|
||||
TopN = 'topN',
|
||||
}
|
||||
|
||||
export enum TableStringCompareTypeEnum {
|
||||
Equal = 'equal',
|
||||
NotEqual = 'notEqual',
|
||||
Contains = 'contains',
|
||||
NotContains = 'notContains',
|
||||
StartsWith = 'startsWith',
|
||||
EndsWith = 'endsWith',
|
||||
}
|
||||
|
||||
export enum TableDateCompareTypeEnum {
|
||||
Equal = 'equal',
|
||||
NotEqual = 'notEqual',
|
||||
After = 'after',
|
||||
AfterOrEqual = 'afterOrEqual',
|
||||
Before = 'before',
|
||||
BeforeOrEqual = 'beforeOrEqual',
|
||||
Between = 'between',
|
||||
NotBetween = 'notBetween',
|
||||
|
||||
Today = 'today',
|
||||
Yesterday = 'yesterday',
|
||||
Tomorrow = 'tomorrow',
|
||||
ThisWeek = 'thisWeek',
|
||||
LastWeek = 'lastWeek',
|
||||
NextWeek = 'nextWeek',
|
||||
ThisMonth = 'thisMonth',
|
||||
LastMonth = 'lastMonth',
|
||||
NextMonth = 'nextMonth',
|
||||
ThisQuarter = 'thisQuarter',
|
||||
LastQuarter = 'lastQuarter',
|
||||
NextQuarter = 'nextQuarter',
|
||||
ThisYear = 'thisYear',
|
||||
LastYear = 'lastYear',
|
||||
NextYear = 'nextYear',
|
||||
// YTD
|
||||
YearToDate = 'yearToDate',
|
||||
|
||||
Quarter = 'quarter',
|
||||
Month = 'month',
|
||||
|
||||
M1 = 'm1',
|
||||
M2 = 'm2',
|
||||
M3 = 'm3',
|
||||
M4 = 'm4',
|
||||
M5 = 'm5',
|
||||
M6 = 'm6',
|
||||
M7 = 'm7',
|
||||
M8 = 'm8',
|
||||
M9 = 'm9',
|
||||
M10 = 'm10',
|
||||
M11 = 'm11',
|
||||
M12 = 'm12',
|
||||
|
||||
Q1 = 'q1',
|
||||
Q2 = 'q2',
|
||||
Q3 = 'q3',
|
||||
Q4 = 'q4',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the pivot cell style type enum
|
||||
*/
|
||||
export enum SheetsTableButtonStateEnum {
|
||||
FilteredSortNone = 1,
|
||||
FilteredSortAsc = 2,
|
||||
FilteredSortDesc = 3,
|
||||
FilterNoneSortNone = 4,
|
||||
FilterNoneSortAsc = 5,
|
||||
FilterNoneSortDesc = 6,
|
||||
}
|
||||
|
||||
export enum SheetsTableSortStateEnum {
|
||||
Asc = 'asc',
|
||||
Desc = 'desc',
|
||||
None = 'none',
|
||||
}
|
||||
289
packages/sheets-table/src/types/type.ts
Normal file
289
packages/sheets-table/src/types/type.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 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, IStyleData, Nullable } from '@univerjs/core';
|
||||
import type { SheetsTableButtonStateEnum, SheetsTableSortStateEnum, TableColumnDataTypeEnum, TableColumnFilterTypeEnum, TableConditionTypeEnum, TableDateCompareTypeEnum, TableNumberCompareTypeEnum, TableStringCompareTypeEnum } from './enum';
|
||||
|
||||
export interface ITableRange {
|
||||
startRow: number;
|
||||
startColumn: number;
|
||||
endRow: number;
|
||||
endColumn: number;
|
||||
}
|
||||
|
||||
export interface ITableOptions {
|
||||
/**
|
||||
* Whether to show the header row.
|
||||
* @todo not support yet
|
||||
*/
|
||||
showHeader?: boolean;
|
||||
/**
|
||||
* Whether to show the footer row.
|
||||
* @todo not support yet
|
||||
*/
|
||||
showFooter?: boolean;
|
||||
/**
|
||||
* The table style id. If the property is empty, the default style will be used.
|
||||
* @default: table-default-0 ~ table-default-5
|
||||
*/
|
||||
tableStyleId?: string;
|
||||
/**
|
||||
* The total number of rows in the table.
|
||||
* @todo not support yet
|
||||
*/
|
||||
hasTotalRow?: boolean;
|
||||
/**
|
||||
* The columns of the table. If not provided, default columns will be generated based on the range.
|
||||
*/
|
||||
columns?: ITableColumnJson[];
|
||||
/**
|
||||
* The column filters of the table.
|
||||
*/
|
||||
filters?: ITableFilterItem[];
|
||||
}
|
||||
|
||||
export interface ITableFilterJSON {
|
||||
tableColumnFilterList?: (ITableFilterItem | undefined)[];
|
||||
tableSortInfo?: { columnIndex: number; sortState: SheetsTableSortStateEnum };
|
||||
}
|
||||
|
||||
export type ITableFilterItem = ITableManualFilterItem | ITableConditionFilterItem | ITableColorFilterItem;
|
||||
export type TableMetaType = Record<string, any>;
|
||||
|
||||
/**
|
||||
* Represents the table filter item, filter data by provided values.
|
||||
*/
|
||||
export interface ITableManualFilterItem {
|
||||
filterType: TableColumnFilterTypeEnum.manual;
|
||||
values: string[];
|
||||
isAllSelected?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the table filter item, filter data by provided condition rules.
|
||||
*/
|
||||
export interface ITableConditionFilterItem {
|
||||
filterType: TableColumnFilterTypeEnum.condition;
|
||||
filterInfo: ITableDateFilterInfo | ITableStringFilterInfo | ITableNumberFilterInfo | ITableLogicFilterInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the table filter item, filter data by cell fill or text colors.
|
||||
*/
|
||||
export interface ITableColorFilterItem {
|
||||
filterType: TableColumnFilterTypeEnum.color;
|
||||
cellFillColors?: Array<string | null>;
|
||||
cellTextColors?: Array<string | null>;
|
||||
}
|
||||
|
||||
export interface ITableDateFilterInfo {
|
||||
conditionType: TableConditionTypeEnum.Date;
|
||||
compareType: TableDateCompareTypeEnum;
|
||||
// Date-like strings
|
||||
expectedValue?: string | [string, string] | Date | [Date, Date];
|
||||
anchorTime?: number;
|
||||
}
|
||||
|
||||
export interface ITableStringFilterInfo {
|
||||
conditionType: TableConditionTypeEnum.String;
|
||||
compareType: TableStringCompareTypeEnum;
|
||||
expectedValue: string;
|
||||
}
|
||||
|
||||
export interface ITableNumberFilterInfo {
|
||||
conditionType: TableConditionTypeEnum.Number;
|
||||
compareType: TableNumberCompareTypeEnum;
|
||||
expectedValue: number | [number, number];
|
||||
}
|
||||
|
||||
export interface ITableLogicFilterInfo {
|
||||
conditionType: TableConditionTypeEnum.Logic;
|
||||
compareType: TableNumberCompareTypeEnum;
|
||||
expectedValue: (ITableDateFilterInfo | ITableStringFilterInfo | ITableNumberFilterInfo)[];
|
||||
}
|
||||
|
||||
export interface ITableAddedEvent {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
tableName: string;
|
||||
range: ITableRange;
|
||||
tableStyleId?: string;
|
||||
}
|
||||
|
||||
export interface ITableRangeUpdate {
|
||||
newRange: IRange;
|
||||
};
|
||||
|
||||
export enum IRangeOperationTypeEnum {
|
||||
Insert = 'insert',
|
||||
Delete = 'delete',
|
||||
};
|
||||
|
||||
export enum IRowColTypeEnum {
|
||||
Row = 'row',
|
||||
Col = 'column',
|
||||
}
|
||||
|
||||
export interface ITableRangeRowColOperation {
|
||||
operationType: IRangeOperationTypeEnum;
|
||||
rowColType: IRowColTypeEnum;
|
||||
index: number;
|
||||
count: number;
|
||||
columnsJson?: ITableColumnJson[];
|
||||
}
|
||||
|
||||
export interface ITableSetConfig {
|
||||
name?: string;
|
||||
updateRange?: ITableRangeUpdate;
|
||||
rowColOperation?: ITableRangeRowColOperation;
|
||||
theme?: string;
|
||||
sortInfo?: ITableFilterJSON['tableSortInfo'];
|
||||
options?: {
|
||||
showHeader?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ITableBaseEvent {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
}
|
||||
export interface ITableNameChangedEvent extends ITableBaseEvent {
|
||||
tableName: string;
|
||||
oldTableName: string;
|
||||
}
|
||||
|
||||
export interface ITableRangeChangedEvent extends ITableBaseEvent {
|
||||
range: ITableRange;
|
||||
oldRange: ITableRange;
|
||||
}
|
||||
|
||||
export interface ITableThemeChangedEvent extends ITableBaseEvent {
|
||||
theme: string;
|
||||
oldTheme: string;
|
||||
}
|
||||
|
||||
export type ITableDeletedEvent = ITableAddedEvent & {
|
||||
tableStyleId?: string;
|
||||
};
|
||||
|
||||
export interface ITableRangeChangedEvent {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
range: ITableRange;
|
||||
oldRange: ITableRange;
|
||||
}
|
||||
|
||||
export interface ITableFilterChangedEvent {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
tableId: string;
|
||||
}
|
||||
export interface ITableColumnJson {
|
||||
dataType: TableColumnDataTypeEnum;
|
||||
id: string;
|
||||
displayName: string;
|
||||
formula: string;
|
||||
meta: TableMetaType;
|
||||
style: IStyleData;
|
||||
}
|
||||
|
||||
export interface ITableJson {
|
||||
id: string;
|
||||
name: string;
|
||||
range: ITableRange;
|
||||
options: ITableOptions;
|
||||
filters: ITableFilterJSON;
|
||||
columns: ITableColumnJson[];
|
||||
meta: TableMetaType;
|
||||
}
|
||||
|
||||
export interface ITableInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
subUnitId: string;
|
||||
range: ITableRange;
|
||||
meta: TableMetaType;
|
||||
columns: ITableColumnJson[];
|
||||
showHeader: boolean;
|
||||
}
|
||||
|
||||
export interface ITableInfoWithUnitId extends ITableInfo {
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
export interface ITableResource {
|
||||
[subUnitId: string]: {
|
||||
tables: ITableJson[];
|
||||
/**
|
||||
* The rows that are filtered out by the table filter
|
||||
*/
|
||||
tableFilteredOutRows: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITableRelationItem {
|
||||
tableId: string;
|
||||
relationColumnId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table relation tuple type, first item is the main table, second item is the sub table
|
||||
*/
|
||||
export type TableRelationTupleType = [ITableRelationItem, ITableRelationItem];
|
||||
|
||||
export type SubTableColumnItem = ITableColumnJson & { tableId: string };
|
||||
|
||||
export interface IDateValue {
|
||||
/**
|
||||
* @description the date value, it stores the number of seconds since January 1, 1970, 00:00:00 UTC, it is same to excel date value
|
||||
*/
|
||||
v: number;
|
||||
/**
|
||||
* @description only date type has this property, we can use it to format the date
|
||||
*/
|
||||
f: string;
|
||||
}
|
||||
type IBlankValue = null | undefined;
|
||||
|
||||
export type IDataFieldValue = string | number | boolean | IDateValue | IBlankValue;
|
||||
|
||||
/**
|
||||
* Represents the table header information, which save the original column id and display name of table.
|
||||
*/
|
||||
export interface IHeadersTableInfoList {
|
||||
tableId: string;
|
||||
columnId: string;
|
||||
displayName: string;
|
||||
}
|
||||
export interface ITableData {
|
||||
headers: string[];
|
||||
headersTableInfoList: IHeadersTableInfoList[];
|
||||
data: Nullable<IDataFieldValue>[][];
|
||||
}
|
||||
|
||||
export interface ICalculatedOptions {
|
||||
average?: number;
|
||||
list?: number[];
|
||||
}
|
||||
|
||||
export interface ITableRangeWithState {
|
||||
range: ITableRange;
|
||||
states: SheetsTableButtonStateEnum[];
|
||||
tableId: string;
|
||||
}
|
||||
152
packages/sheets-table/src/util.ts
Normal file
152
packages/sheets-table/src/util.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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, IUniverInstanceService, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import type { TableManager } from './models/table-manager';
|
||||
import type {
|
||||
ITableColorFilterItem,
|
||||
ITableConditionFilterItem,
|
||||
ITableFilterItem,
|
||||
ITableManualFilterItem,
|
||||
} from './types/type';
|
||||
import { CellValueType, UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetsTableButtonStateEnum, SheetsTableSortStateEnum, TableColumnFilterTypeEnum } from './types/enum';
|
||||
|
||||
export function getColumnName(columnIndex: number, columnText: string): string {
|
||||
return `${columnText} ${columnIndex}`;
|
||||
}
|
||||
|
||||
const BooleanTrue = 'TRUE';
|
||||
const BooleanFalse = 'FALSE';
|
||||
export const getStringFromDataStream = (data: IDocumentData): string => {
|
||||
const dataStream = data.body?.dataStream.replace(/\r\n$/, '') || '';
|
||||
return dataStream;
|
||||
};
|
||||
|
||||
/**
|
||||
* transform cell data to dimension name
|
||||
* @param cellData the sheet cell data
|
||||
* @param styles workBook styles collection
|
||||
* @param patternInfoRecord The cache record for pattern info
|
||||
* @returns {string} The dimension name
|
||||
*/
|
||||
export function convertCellDataToString(cellData: Nullable<ICellData>): string {
|
||||
if (cellData) {
|
||||
const { v, t, p } = cellData;
|
||||
if (p) {
|
||||
return getStringFromDataStream(p);
|
||||
}
|
||||
if ((t === CellValueType.FORCE_STRING || t === CellValueType.STRING) && v !== undefined && v !== null) {
|
||||
return String(v);
|
||||
} else if (t === CellValueType.BOOLEAN) {
|
||||
return v ? BooleanTrue : BooleanFalse;
|
||||
} else if (t === CellValueType.NUMBER) {
|
||||
return String(v);
|
||||
} else {
|
||||
// type not set, guess the type is number
|
||||
const type = typeof v;
|
||||
if (type === 'boolean') {
|
||||
return v ? BooleanTrue : BooleanFalse;
|
||||
}
|
||||
return v === undefined || v === null ? '' : String(v);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getTableFilterState(tableFilter: ITableFilterItem | undefined, sortState: SheetsTableSortStateEnum): SheetsTableButtonStateEnum {
|
||||
const hasFilter = tableFilter !== undefined && tableFilter !== null;
|
||||
if (hasFilter) {
|
||||
switch (sortState) {
|
||||
case SheetsTableSortStateEnum.Asc:
|
||||
return SheetsTableButtonStateEnum.FilteredSortAsc;
|
||||
case SheetsTableSortStateEnum.Desc:
|
||||
return SheetsTableButtonStateEnum.FilteredSortDesc;
|
||||
default:
|
||||
return SheetsTableButtonStateEnum.FilteredSortNone;
|
||||
}
|
||||
} else {
|
||||
switch (sortState) {
|
||||
case SheetsTableSortStateEnum.Asc:
|
||||
return SheetsTableButtonStateEnum.FilterNoneSortAsc;
|
||||
case SheetsTableSortStateEnum.Desc:
|
||||
return SheetsTableButtonStateEnum.FilterNoneSortDesc;
|
||||
default:
|
||||
return SheetsTableButtonStateEnum.FilterNoneSortNone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isConditionFilter(filter: ITableFilterItem | undefined): filter is ITableConditionFilterItem {
|
||||
if (!filter) {
|
||||
return false;
|
||||
}
|
||||
return filter.filterType === TableColumnFilterTypeEnum.condition;
|
||||
}
|
||||
|
||||
export function isManualTableFilter(filter: ITableFilterItem | undefined): filter is ITableManualFilterItem {
|
||||
if (!filter) {
|
||||
return false;
|
||||
}
|
||||
return filter.filterType === TableColumnFilterTypeEnum.manual;
|
||||
}
|
||||
|
||||
export function isColorTableFilter(filter: ITableFilterItem | undefined): filter is ITableColorFilterItem {
|
||||
if (!filter) {
|
||||
return false;
|
||||
}
|
||||
return filter.filterType === TableColumnFilterTypeEnum.color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing names including sheet names, table names and defined names to check for duplicates table name.
|
||||
*/
|
||||
export function getExistingNamesSet(unitId: string, options: {
|
||||
univerInstanceService?: IUniverInstanceService;
|
||||
tableManager?: TableManager;
|
||||
definedNamesService?: IDefinedNamesService;
|
||||
}): Set<string> {
|
||||
const { univerInstanceService, tableManager, definedNamesService } = options;
|
||||
// The set to store existing names, case insensitive
|
||||
const existingNamesSet = new Set<string>();
|
||||
|
||||
// The table names can't be duplicate with existing sheet names.
|
||||
const workbook = univerInstanceService?.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (workbook) {
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
existingNamesSet.add(sheet.getName().toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// The table names can't be duplicate with existing table names.
|
||||
const tableList = tableManager?.getTableList(unitId);
|
||||
if (tableList && tableList.length > 0) {
|
||||
tableList.forEach((tableItem) => {
|
||||
existingNamesSet.add(tableItem.name.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// The table names can't be duplicate with existing defined names.
|
||||
const definedNames = definedNamesService?.getDefinedNameMap(unitId);
|
||||
if (definedNames) {
|
||||
Object.values(definedNames).forEach((definedName) => {
|
||||
existingNamesSet.add(definedName.name.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
return existingNamesSet;
|
||||
}
|
||||
41
packages/sheets-table/src/util/table-name.ts
Normal file
41
packages/sheets-table/src/util/table-name.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { customNameCharacterCheck } from '@univerjs/core';
|
||||
|
||||
export type SheetTableNameValidationReason = 'empty' | 'invalid';
|
||||
|
||||
export interface ISheetTableNameValidationResult {
|
||||
valid: boolean;
|
||||
reason?: SheetTableNameValidationReason;
|
||||
}
|
||||
|
||||
export function validateSheetTableName(name: string, existingNamesSet: Set<string>): ISheetTableNameValidationResult {
|
||||
const trimmedName = name.trim();
|
||||
|
||||
if (!trimmedName) {
|
||||
return { valid: false, reason: 'empty' };
|
||||
}
|
||||
|
||||
const normalizedExistingNames = new Set(Array.from(existingNamesSet, (item) => item.toLowerCase()));
|
||||
const isValidName = customNameCharacterCheck(trimmedName.toLowerCase(), normalizedExistingNames);
|
||||
|
||||
if (!isValidName) {
|
||||
return { valid: false, reason: 'invalid' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
8
packages/sheets-table/tsconfig.json
Normal file
8
packages/sheets-table/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/sheets-table/tsconfig.node.json
Normal file
18
packages/sheets-table/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/sheets-table/vitest.config.ts
Normal file
3
packages/sheets-table/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user