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

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

View File

@@ -0,0 +1,50 @@
# @univerjs/sheets-table
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-table?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-table)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-table?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-table)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-table?style=flat-square)](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)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: 'عمود',
tablePrefix: 'جدول',
tableNameError: 'لا يمكن أن يحتوي اسم الجدول على مسافات، ولا يمكن أن يبدأ برقم، ولا يمكن أن يكون مطابقًا لاسم جدول موجود',
},
};
export default locale;

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: 'ستون',
tablePrefix: 'جدول',
tableNameError: 'نام جدول نمی‌تواند شامل فاصله باشد، نمی‌تواند با عدد شروع شود و نمی‌تواند با نام جدول‌های موجود تکراری باشد',
},
};
export default locale;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: '列',
tablePrefix: '表',
tableNameError: '表の名前は空白を含めることはできず、数字で始めることはできず、既存の表名と重複できません',
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: '열',
tablePrefix: '표',
tableNameError: '표 이름은 공백을 포함할 수 없으며 숫자로 시작할 수 없고 기존 표 이름과 중복될 수 없습니다',
},
};
export default locale;

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: 'Столбец',
tablePrefix: 'Таблица',
tableNameError: 'Имя таблицы не может содержать пробелы, не может начинаться с цифры и не может совпадать с именем существующей таблицы',
},
};
export default locale;

View File

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

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: '列',
tablePrefix: '表格',
tableNameError: '表格名称不能包含空格, 不能以数字开头,不能和已有表格名称重复',
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: '列',
tablePrefix: '表格',
tableNameError: '表格名稱不能包含空格, 不能以數字開頭,不能和已有表格名稱重複',
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-table': {
columnPrefix: '列',
tablePrefix: '表格',
tableNameError: '表格名稱不能包含空格, 不能以數字開頭,不能和已有表格名稱重複',
},
};
export default locale;

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

View 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 = [];
}
}

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

View 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 = [];
}
}

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

View File

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

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

View 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',
}

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

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

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

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View 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"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();