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,41 @@
# @univerjs/sheets-sort
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-sort?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-sort)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-sort?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-sort)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-sort?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-sort)
`@univerjs/sheets-sort` adds the core sorting model, commands, and services for Univer Sheets.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets-sort` | `UniverSheetsSort` | No | No | Yes |
## Installation
```sh
pnpm add @univerjs/sheets-sort
# or
npm install @univerjs/sheets-sort
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort';
univer.registerPlugin(UniverSheetsSortPlugin);
```
## Integration Notes
Use this package with `@univerjs/sheets-sort-ui` when users need sorting menus and panels.
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets-sort)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,87 @@
{
"name": "@univerjs/sheets-sort",
"version": "1.0.0-rc.0",
"private": false,
"description": "Sorting model, commands, and services 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",
"sort",
"spreadsheet",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./facade": "./src/facade/index.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./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"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/sheets": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
import {
BooleanNumber,
ILogService,
Inject,
Injector,
IUniverInstanceService,
LocaleType,
LogLevel,
Plugin,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { FormulaDataModel } from '@univerjs/engine-formula';
import { SheetInterceptorService, SheetSkeletonService, SheetsSelectionsService } from '@univerjs/sheets';
import { SheetsSortController } from '../../../controllers/sheets-sort.controller';
import { SheetsSortService } from '../../../services/sheets-sort.service';
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
name: 'sheet1',
cellData: {
0: {
0: {
v: 1,
},
1: {
v: 20,
},
2: {
v: 100,
},
},
1: {
0: {
v: 2,
},
1: {
v: 19,
},
2: {
v: 100,
},
},
2: {
0: {
v: 3,
},
1: {
v: 18,
},
2: {
v: 100,
},
},
3: {
0: {
v: 4,
},
1: {
v: 17,
},
2: {
v: 200,
},
},
4: {
0: {
v: 5,
},
1: {
v: 16,
},
2: {
v: 200,
},
},
5: {
0: {
v: 6,
},
1: {
v: 15,
},
2: {
v: 200,
},
},
},
columnData: {
1: {
hd: BooleanNumber.FALSE,
},
},
rowData: {
1: {
hd: BooleanNumber.FALSE,
},
},
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
export interface ITestBed {
univer: Univer;
get: Injector['get'];
sheet: Workbook;
}
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
const univer = new Univer();
const injector = univer.__getInjector();
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
const injector = this._injector;
injector.add([SheetsSortService]);
injector.add([SheetsSortController]);
injector.add([SheetsSelectionsService, { useClass: mockSelectionManagerService as any }]);
injector.add([SheetInterceptorService]);
injector.add([SheetSkeletonService]);
injector.add([FormulaDataModel, { useClass: mockFormulaDataModel }]);
dependencies?.forEach((d) => injector.add(d));
}
override onReady(): void {
this._injector.get(SheetsSortController);
}
}
univer.registerPlugin(TestPlugin);
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, Tools.deepClone(workbookData || TEST_WORKBOOK_DATA_DEMO));
const univerInstanceService = injector.get(IUniverInstanceService);
univerInstanceService.focusUnit('test');
const logService = injector.get(ILogService);
logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs
return {
univer,
get: injector.get.bind(injector),
sheet,
};
}
class mockSelectionManagerService {
replace() {
}
}
class mockFormulaDataModel {
getArrayFormulaRange() {
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellData, Injector, Nullable, Univer, Workbook } from '@univerjs/core';
import type { ISortRangeCommandParams } from '../sheets-sort.command';
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
import { ReorderRangeCommand, ReorderRangeMutation, SetSelectionsOperation } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SortRangeCommand } from '../sheets-sort.command';
import { createCommandTestBed } from './create-command-test-bed';
describe('Test "Sort Range Commands"', () => {
let univer: Univer;
let get: Injector['get'];
let commandService: ICommandService;
let getData: (row: number, col: number) => Nullable<ICellData>;
beforeEach(() => {
const testBed = createCommandTestBed();
univer = testBed.univer;
get = testBed.get;
commandService = get(ICommandService);
commandService.registerCommand(SetSelectionsOperation);
commandService.registerCommand(ReorderRangeMutation);
commandService.registerCommand(ReorderRangeCommand);
const univerInstanceService = get(IUniverInstanceService);
const workbook = univerInstanceService.getUnit('test') as Workbook;
const worksheet = workbook?.getSheetBySheetId('sheet1')!;
getData = (row: number, col: number) => {
const data = worksheet.getCellMatrix().getValue(row, col);
if (data) {
return data;
}
return undefined;
};
});
afterEach(() => univer.dispose());
describe('sort with single col', () => {
it('asc case: ', async () => {
const params = {
unitId: 'test',
subUnitId: 'sheet1',
hasTitle: false,
orderRules: [{ colIndex: 1, type: 'asc' }],
range: {
startRow: 0,
endRow: 5,
startColumn: 0,
endColumn: 2,
},
} as ISortRangeCommandParams;
const result = await commandService.executeCommand(SortRangeCommand.id, params);
expect(result).toBeTruthy();
expect(getData(0, 0)?.v).toBe(6);
expect(getData(0, 1)?.v).toBe(15);
expect(getData(0, 2)?.v).toBe(200);
expect(getData(5, 0)?.v).toBe(1);
expect(getData(5, 1)?.v).toBe(20);
expect(getData(5, 2)?.v).toBe(100);
});
it('desc case: ', async () => {
const params = {
unitId: 'test',
subUnitId: 'sheet1',
hasTitle: false,
orderRules: [{ colIndex: 0, type: 'desc' }],
range: {
startRow: 0,
endRow: 5,
startColumn: 0,
endColumn: 2,
},
} as ISortRangeCommandParams;
const result = await commandService.executeCommand(SortRangeCommand.id, params);
expect(result).toBeTruthy();
expect(getData(0, 0)?.v).toBe(6);
expect(getData(0, 1)?.v).toBe(15);
expect(getData(0, 2)?.v).toBe(200);
expect(getData(5, 0)?.v).toBe(1);
expect(getData(5, 1)?.v).toBe(20);
expect(getData(5, 2)?.v).toBe(100);
});
});
describe('sort with multiple cols', () => {
it('asc + desc combine case', async () => {
const params = {
unitId: 'test',
subUnitId: 'sheet1',
hasTitle: false,
orderRules: [{ colIndex: 2, type: 'desc' }, { colIndex: 1, type: 'asc' }],
range: {
startRow: 0,
endRow: 5,
startColumn: 0,
endColumn: 2,
},
} as ISortRangeCommandParams;
const result = await commandService.executeCommand(SortRangeCommand.id, params);
expect(result).toBeTruthy();
expect(getData(0, 0)?.v).toBe(6);
expect(getData(0, 1)?.v).toBe(15);
expect(getData(0, 2)?.v).toBe(200);
expect(getData(5, 0)?.v).toBe(1);
expect(getData(5, 1)?.v).toBe(20);
expect(getData(5, 2)?.v).toBe(100);
});
});
});

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.
*/
// This file provides a ton of mutations to manipulate `FilterModel`.
// These models would be held on `SheetsFilterService`.
import type { IAccessor, ICellData, ICommand, IRange, Nullable, Worksheet } from '@univerjs/core';
import type { IReorderRangeCommandParams, ISheetCommandSharedParams } from '@univerjs/sheets';
import type { IOrderRule, SortType } from '../../services/interface';
import { CommandType, ICommandService, IUniverInstanceService, Rectangle, sequenceExecute } from '@univerjs/core';
import { getSheetCommandTarget, ReorderRangeCommand } from '@univerjs/sheets';
import { SheetsSortService } from '../../services/sheets-sort.service';
export interface ISortRangeCommandParams extends ISheetCommandSharedParams {
range: IRange;
orderRules: IOrderRule[];
hasTitle: boolean;
}
export interface IRowComparator {
index: number;
value: Array<Nullable<ICellData>>;
}
export enum ORDER {
KEEP = 1,
EXCHANGE = -1,
EQUAL = 0,
}
export type CellValue = number | string | null;
export type ICellValueCompareFn = (type: SortType, a: Nullable<ICellData>, b: Nullable<ICellData>) => Nullable<number>;
export const SortRangeCommand: ICommand = {
id: 'sheet.command.sort-range',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISortRangeCommandParams) => {
const { range, orderRules, hasTitle, unitId, subUnitId } = params;
const sortService = accessor.get(SheetsSortService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const { worksheet } = getSheetCommandTarget(univerInstanceService, params) || {};
if (!worksheet) {
return false;
}
const mergeDataInRange = worksheet.getMergeData().filter((mergeData) => {
return Rectangle.contains(range, mergeData);
});
const mergeMainRowIndexes = mergeDataInRange.map((mergeData) => {
return mergeData.startRow;
});
const { startRow: rangeStartRow, endRow } = range;
const startRow = hasTitle ? rangeStartRow + 1 : rangeStartRow;
const toReorder: IRowComparator[] = [];
const oldOrder: number[] = [];
for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) {
if (worksheet.getRowFiltered(rowIndex)) {
continue;
}
if (worksheet.getRowRawVisible(rowIndex) === false) {
continue;
}
if (mergeDataInRange.length && !mergeMainRowIndexes.includes(rowIndex)) {
continue;
}
toReorder.push({
index: rowIndex,
value: getRowCellData(worksheet, rowIndex, orderRules),
});
oldOrder.push(rowIndex);
}
const compareFns: ICellValueCompareFn[] = sortService.getAllCompareFns();
toReorder.sort(reorderFnGenerator(orderRules, combineCompareFnsAsOne(compareFns)));
const order: { [key: number]: number } = {};
toReorder.forEach(({ index, value }, oldIndex) => {
order[oldOrder[oldIndex]] = index;
});
const reorderRangeCommand = {
id: ReorderRangeCommand.id,
params: {
unitId,
subUnitId,
range,
order,
} as unknown as IReorderRangeCommandParams,
};
const commandService = accessor.get(ICommandService);
const res = sequenceExecute([reorderRangeCommand], commandService);
return res.result;
},
};
function getRowCellData(
worksheet: Worksheet,
rowIndex: number,
orderRules: IOrderRule[]
): Nullable<ICellData>[] {
const result: Nullable<ICellData>[] = [];
orderRules.forEach(({ colIndex }) => {
result.push(worksheet.getCellRaw(rowIndex, colIndex));
});
return result;
}
function combineCompareFnsAsOne(compareFns: ICellValueCompareFn[]) {
return (type: SortType, a: Nullable<ICellData>, b: Nullable<ICellData>) => {
for (let i = 0; i < compareFns.length; i++) {
const res = compareFns[i](type, a, b);
// null means can't compare in this fn.
if (res != null) {
return res;
}
}
// All fns can't compare these two value, means equal.
return 0;
};
}
function reorderFnGenerator(orderRules: IOrderRule[], valueCompare: ICellValueCompareFn) {
return function (a: IRowComparator, b: IRowComparator): number {
let ret: Nullable<number> = null;
for (let index = 0; index < orderRules.length; index++) {
const aCellData = a.value[index];
const bCellData = b.value[index];
ret = valueCompare(orderRules[index].type, aCellData, bCellData);
if (ret !== 0 && ret !== null && ret !== undefined) {
return ret as number;
}
}
return 0;
};
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEETS_SORT_PLUGIN_CONFIG_KEY = 'sheets-sort.config';
export const configSymbol = Symbol(SHEETS_SORT_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsSortConfig {
}
export const defaultPluginConfig: IUniverSheetsSortConfig = {};

View File

@@ -0,0 +1,92 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellData, Nullable } from '@univerjs/core';
import type { ICellValueCompareFn } from '../commands/commands/sheets-sort.command';
import { CellValueType, Disposable, ICommandService, Inject } from '@univerjs/core';
import { SortRangeCommand } from '../commands/commands/sheets-sort.command';
import { SheetsSortService } from '../services/sheets-sort.service';
import { compareNull, compareNumber, compareString, isNullValue } from './utils';
export type ICommonComparableCellValue = number | string | null;
export class SheetsSortController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@Inject(SheetsSortService) private readonly _sortService: SheetsSortService
) {
super();
this._initCommands();
this._registerCompareFns();
}
private _initCommands(): void {
[
SortRangeCommand,
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
}
private _registerCompareFns(): void {
const commonFn: ICellValueCompareFn = (type, a, b) => {
const valueA: ICommonComparableCellValue = this._getCommonValue(a);
const valueB: ICommonComparableCellValue = this._getCommonValue(b);
const compareTypeFns = [
compareNull,
compareString,
compareNumber,
];
for (let i = 0; i < compareTypeFns.length; i++) {
const res = compareTypeFns[i](valueA, valueB, type);
if (res !== null) {
return res;
}
}
return null;
};
this._sortService.registerCompareFn(commonFn);
}
private _getCommonValue(a: Nullable<ICellData>): ICommonComparableCellValue {
if (isNullValue(a)) {
return null;
}
const richTextValue = a?.p?.body?.dataStream;
if (richTextValue) {
return richTextValue;
}
if (a?.t === CellValueType.NUMBER) {
return Number.parseFloat(`${a.v}`);
}
if (a?.t === CellValueType.STRING) {
if (typeof a.v === 'number') {
return a.v;
}
return `${a.v}`;
}
if (a?.t === CellValueType.BOOLEAN) {
return `${a.v}`;
}
if (a?.t === CellValueType.FORCE_STRING) {
return Number.parseFloat(`${a.v}`);
}
return `${a?.v}`;
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellData, Nullable } from '@univerjs/core';
import type { ICommonComparableCellValue } from './sheets-sort.controller';
import { SortType } from '../services/interface';
export enum ORDER {
POSITIVE = 1,
NEGATIVE = -1,
ZERO = 0,
}
const removeStringSymbol = (str: string) => {
return str.replace(/-/gi, '').replace(/'/gi, '');
};
export const compareNull = (
a1: ICommonComparableCellValue,
a2: ICommonComparableCellValue
) => {
const isA1Null = a1 === null || a1 === '';
const isA2Null = a2 === null || a2 === '';
if (isA1Null && isA2Null) return ORDER.ZERO;
if (isA1Null) return ORDER.POSITIVE;
if (isA2Null) return ORDER.NEGATIVE;
return null;
};
export const compareNumber = (
a1: ICommonComparableCellValue,
a2: ICommonComparableCellValue,
type: SortType
) => {
const isA1Num = typeof a1 === 'number';
const isA2Num = typeof a2 === 'number';
if (isA1Num && isA2Num) {
if (a1 < a2) {
return type === SortType.ASC ? ORDER.NEGATIVE : ORDER.POSITIVE;
}
if (a1 > a2) {
return type === SortType.ASC ? ORDER.POSITIVE : ORDER.NEGATIVE;
}
return ORDER.ZERO;
}
if (isA1Num) {
return type === SortType.ASC ? ORDER.POSITIVE : ORDER.NEGATIVE;
}
if (isA2Num) {
return type === SortType.ASC ? ORDER.NEGATIVE : ORDER.POSITIVE;
}
return null;
};
export const compareString = (a1: ICommonComparableCellValue, a2: ICommonComparableCellValue, type: SortType) => {
const isA1Str = typeof a1 === 'string';
const isA2Str = typeof a2 === 'string';
if (isA1Str) {
a1 = removeStringSymbol((a1 as string).toLocaleLowerCase()) as string;
}
if (isA2Str) {
a2 = removeStringSymbol((a2 as string).toLocaleLowerCase()) as string;
}
if (!isA1Str && !isA2Str) {
return null;
}
if (isA1Str && isA2Str) {
const a1AsString = a1 as string;
const a2AsString = a2 as string;
if (a1AsString < a2AsString) {
return type === SortType.ASC ? ORDER.NEGATIVE : ORDER.POSITIVE;
}
if (a1AsString > a2AsString) {
return type === SortType.ASC ? ORDER.POSITIVE : ORDER.NEGATIVE;
}
return ORDER.ZERO;
}
if (isA1Str) {
return type === SortType.ASC ? ORDER.POSITIVE : ORDER.NEGATIVE;
}
if (isA2Str) {
return type === SortType.ASC ? ORDER.NEGATIVE : ORDER.POSITIVE;
}
return null;
};
export const isNullValue = (cell: Nullable<ICellData>) => {
if (!cell) {
return true;
}
if (Object.keys(cell).length === 0) {
return true;
}
if (cell?.v == null && cell?.p == null) {
return true;
}
return false;
};

View File

@@ -0,0 +1,94 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellData, Injector, Nullable, Univer, Workbook } from '@univerjs/core';
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { DefinedNamesService, IDefinedNamesService } from '@univerjs/engine-formula';
import { ReorderRangeCommand, ReorderRangeMutation, SetSelectionsOperation } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createCommandTestBed } from '../../commands/commands/__tests__/create-command-test-bed';
import '@univerjs/sheets/facade';
import '@univerjs/sheets-sort/facade';
describe('sheets-sort facade', () => {
let univer: Univer;
let get: Injector['get'];
let univerAPI: FUniver;
let getData: (row: number, col: number) => Nullable<ICellData>;
beforeEach(() => {
const testBed = createCommandTestBed(undefined, [
[IDefinedNamesService, { useClass: DefinedNamesService }],
]);
univer = testBed.univer;
get = testBed.get;
const commandService = get(ICommandService);
commandService.registerCommand(SetSelectionsOperation);
commandService.registerCommand(ReorderRangeMutation);
commandService.registerCommand(ReorderRangeCommand);
const workbook = get(IUniverInstanceService).getUnit<Workbook>('test')!;
const worksheet = workbook.getSheetBySheetId('sheet1')!;
getData = (row: number, col: number) => worksheet.getCellMatrix().getValue(row, col) ?? undefined;
univerAPI = FUniver.newAPI(univer.__getInjector());
});
afterEach(() => {
univer.dispose();
});
it('sorts a selected range by a facade column spec relative to the range', () => {
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
worksheet.getRange('A1:C6').sort({ column: 1, ascending: true });
expect(getData(0, 0)?.v).toBe(6);
expect(getData(0, 1)?.v).toBe(15);
expect(getData(5, 0)?.v).toBe(1);
expect(getData(5, 1)?.v).toBe(20);
});
it('sorts the whole worksheet and emits before/after sort events with user-facing columns', () => {
const events: Array<{ phase: string; column: number; ascending: boolean }> = [];
const beforeDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeSort, (params) => {
events.push({
phase: 'before',
column: params.sortColumn[0].column,
ascending: params.sortColumn[0].ascending,
});
});
const afterDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeSorted, (params) => {
events.push({
phase: 'after',
column: params.sortColumn[0].column,
ascending: params.sortColumn[0].ascending,
});
});
univerAPI.getActiveWorkbook()!.getActiveSheet().sort(0, false);
expect(getData(0, 0)?.v).toBe(6);
expect(events).toEqual([
{ phase: 'before', column: 0, ascending: false },
{ phase: 'after', column: 0, ascending: false },
]);
beforeDisposable.dispose();
afterDisposable.dispose();
});
});

View File

@@ -0,0 +1,89 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IEventBase } from '@univerjs/core/facade';
import type { FRange, FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import { FEventName } from '@univerjs/core/facade';
/**
* @ignore
*/
export interface IFSheetsSortEventNameMixin {
/**
* This event will be emitted when a range on a worksheet is sorted.
* @see {@link ISheetRangeSortParams}
* @example
* ```typescript
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeSorted, (params) => {
* console.log(params);
* const { workbook, worksheet, range, sortColumn } = params;
* });
*
* // Remove the event listener, use `callbackDisposable.dispose()`
* ```
*/
SheetRangeSorted: 'SheetRangeSorted';
/**
* This event will be emitted before sorting a range on a worksheet.
* @see {@link ISheetRangeSortParams}
* @example
* ```typescript
* const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeSort, (params) => {
* console.log(params);
* const { workbook, worksheet, range, sortColumn } = params;
*
* // Cancel the sorting operation.
* params.cancel = true;
* });
*
* // Remove the event listener, use `callbackDisposable.dispose()`
* ```
*/
SheetBeforeRangeSort: 'SheetBeforeRangeSort';
}
export class FSheetsSortEventNameMixin implements IFSheetsSortEventNameMixin {
get SheetRangeSorted(): 'SheetRangeSorted' { return 'SheetRangeSorted' as const; }
get SheetBeforeRangeSort(): 'SheetBeforeRangeSort' { return 'SheetBeforeRangeSort' as const; }
}
interface ISortColumn {
column: number;
ascending: boolean;
}
export interface ISheetsRangeSortEventParams extends IEventBase {
workbook: FWorkbook;
worksheet: FWorksheet;
range: FRange;
sortColumn: ISortColumn[];
}
/**
* @ignore
*/
export interface ISheetsSortEventParamConfig {
SheetBeforeRangeSort: ISheetsRangeSortEventParams;
SheetRangeSorted: ISheetsRangeSortEventParams;
}
FEventName.extend(FSheetsSortEventNameMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends IFSheetsSortEventNameMixin { }
interface IEventParamConfig extends ISheetsSortEventParamConfig { }
}

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.
*/
import type { IOrderRule } from '@univerjs/sheets-sort';
import { SortRangeCommand, SortType } from '@univerjs/sheets-sort';
import { FRange } from '@univerjs/sheets/facade';
export type SortColumnSpec = { column: number; ascending: boolean } | number;
/**
* @ignore
*/
export interface IFRangeSheetsSortMixin {
/**
* Sorts the cells in the given range, by column(s) and order specified.
* @param {SortColumnSpec | SortColumnSpec[]} column The column index with order or an array of column indexes with order. The range first column index is 0.
* @returns {FRange} The range itself for chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('D1:G10');
*
* // Sorts the range by the first column in ascending order.
* fRange.sort(0);
*
* // Sorts the range by the first column in descending order.
* fRange.sort({ column: 0, ascending: false });
*
* // Sorts the range by the first column in descending order and the second column in ascending order.
* fRange.sort([{ column: 0, ascending: false }, 1]);
* ```
*/
sort(column: SortColumnSpec | SortColumnSpec[]): FRange;
}
export class FRangeSheetsSortMixin extends FRange implements IFRangeSheetsSortMixin {
override sort(column: SortColumnSpec | SortColumnSpec[]): FRange {
const columnBase = this._range.startColumn;
const columns = Array.isArray(column) ? column : [column];
const orderRules: IOrderRule[] = columns.map((c) => {
if (typeof c === 'number') {
return { colIndex: c + columnBase, type: SortType.ASC };
}
return {
colIndex: c.column + columnBase,
type: c.ascending ? SortType.ASC : SortType.DESC,
};
});
this._commandService.syncExecuteCommand(SortRangeCommand.id, {
orderRules,
range: this._range,
hasTitle: false,
unitId: this._workbook.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
});
return this;
}
}
FRange.extend(FRangeSheetsSortMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FRange extends IFRangeSheetsSortMixin {}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommandInfo, Injector } from '@univerjs/core';
import type { ISortRangeCommandParams } from '@univerjs/sheets-sort';
import type { ISheetsRangeSortEventParams } from './f-event';
import { ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { SortRangeCommand, SortType } from '@univerjs/sheets-sort';
export class FUniverSheetsSortMixin extends FUniver {
/**
* @ignore
*/
override _initialize(injector: Injector): void {
const commandService = injector.get(ICommandService);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetBeforeRangeSort,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== SortRangeCommand.id) return;
this._beforeRangeSort(commandInfo as Readonly<ICommandInfo<ISortRangeCommandParams>>);
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.SheetRangeSorted,
() => commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id !== SortRangeCommand.id) return;
this._onRangeSorted(commandInfo as Readonly<ICommandInfo<ISortRangeCommandParams>>);
})
)
);
}
private _beforeRangeSort(commandInfo: Readonly<ICommandInfo<ISortRangeCommandParams>>): void {
const params = commandInfo.params!;
const fWorkbook = this.getWorkbook(params.unitId)!;
const fWorksheet = fWorkbook.getSheetBySheetId(params.subUnitId)!;
const { startColumn, endColumn, startRow, endRow } = params.range;
const fRange = fWorksheet.getRange(startRow, startColumn, endRow - startRow + 1, endColumn - startColumn + 1);
const eventParams: ISheetsRangeSortEventParams = {
workbook: fWorkbook,
worksheet: fWorksheet,
range: fRange,
sortColumn: params.orderRules.map((rule) => ({
column: rule.colIndex - startColumn,
ascending: rule.type === SortType.ASC,
})),
};
this.fireEvent(this.Event.SheetBeforeRangeSort, eventParams);
if (eventParams.cancel) {
throw new Error('SortRangeCommand canceled.');
}
}
private _onRangeSorted(commandInfo: Readonly<ICommandInfo<ISortRangeCommandParams>>): void {
const params = commandInfo.params!;
const fWorkbook = this.getWorkbook(params.unitId)!;
const fWorksheet = fWorkbook.getSheetBySheetId(params.subUnitId)!;
const { startColumn, endColumn, startRow, endRow } = params.range;
const fRange = fWorksheet.getRange(startRow, startColumn, endRow - startRow + 1, endColumn - startColumn + 1);
const eventParams: ISheetsRangeSortEventParams = {
workbook: fWorkbook,
worksheet: fWorksheet,
range: fRange,
sortColumn: params.orderRules.map((rule) => ({
column: rule.colIndex - startColumn,
ascending: rule.type === SortType.ASC,
})),
};
this.fireEvent(this.Event.SheetRangeSorted, eventParams);
if (eventParams.cancel) {
throw new Error('SortRangeCommand canceled.');
}
}
}
FUniver.extend(FUniverSheetsSortMixin);

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 { IRange } from '@univerjs/core';
import type { IOrderRule } from '@univerjs/sheets-sort';
import { RANGE_TYPE } from '@univerjs/core';
import { SortRangeCommand, SortType } from '@univerjs/sheets-sort';
import { FWorksheet } from '@univerjs/sheets/facade';
/**
* @ignore
*/
export interface IFWorksheetSortMixin {
/**
* Sort the worksheet by the specified column.
* @param {number} colIndex The column index to sort by.
* @param {boolean} [asc=true] The sort order. `true` for ascending, `false` for descending. The column A index is 0.
* @returns {FWorksheet} The worksheet itself for chaining.
* @example
* ```typescript
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Sorts the worksheet by the column A in ascending order.
* fWorksheet.sort(0);
*
* // Sorts the worksheet by the column A in descending order.
* fWorksheet.sort(0, false);
* ```
*/
sort(colIndex: number, asc?: boolean): FWorksheet;
}
export class FWorksheetSortMixin extends FWorksheet implements IFWorksheetSortMixin {
override sort(colIndex: number, asc = true): FWorksheet {
const orderRules: IOrderRule[] = [{
colIndex,
type: asc ? SortType.ASC : SortType.DESC,
}];
const range: IRange = {
startRow: 0,
startColumn: 0,
endRow: this._worksheet.getRowCount() - 1,
endColumn: this._worksheet.getColumnCount() - 1,
rangeType: RANGE_TYPE.ALL,
};
this._commandService.syncExecuteCommand(SortRangeCommand.id, {
orderRules,
range,
hasTitle: false,
unitId: this._workbook.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
});
return this;
}
}
FWorksheet.extend(FWorksheetSortMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FWorksheet extends IFWorksheetSortMixin {}
}

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.
*/
import './f-univer';
import './f-range';
import './f-worksheet';
import './f-event';
export type * from './f-event';
export type * from './f-range';
export type * from './f-univer';
export type * from './f-worksheet';

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { SortRangeCommand } from './commands/commands/sheets-sort.command';
export type { ISortRangeCommandParams } from './commands/commands/sheets-sort.command';
export type { IUniverSheetsSortConfig } from './config/config';
export { SheetsSortController } from './controllers/sheets-sort.controller';
export { UniverSheetsSortPlugin } from './plugin';
export { SortType } from './services/interface';
export type { IOrderRule, ISortOption } from './services/interface';
export { SheetsSortService } from './services/sheets-sort.service';

View File

@@ -0,0 +1,60 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import type { IUniverSheetsSortConfig } from './config/config';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import pkg from '../package.json';
import { defaultPluginConfig, SHEETS_SORT_PLUGIN_CONFIG_KEY } from './config/config';
import { SheetsSortController } from './controllers/sheets-sort.controller';
import { SheetsSortService } from './services/sheets-sort.service';
@DependentOn(UniverFormulaEnginePlugin, UniverSheetsPlugin)
export class UniverSheetsSortPlugin extends Plugin {
static override type = UniverInstanceType.UNIVER_SHEET;
static override pluginName = 'SHEET_SORT_PLUGIN';
static override packageName = pkg.name;
static override version = pkg.version;
constructor(
private readonly _config: Partial<IUniverSheetsSortConfig> = defaultPluginConfig,
@Inject(Injector) protected readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SHEETS_SORT_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[SheetsSortController],
[SheetsSortService],
] as Dependency[]).forEach((d) => this._injector.add(d));
}
override onReady(): void {
this._injector.get(SheetsSortController);
}
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellValueCompareFn } from '../../commands/commands/sheets-sort.command';
import { ICommandService, Injector, IUniverInstanceService } from '@univerjs/core';
import { FormulaDataModel } from '@univerjs/engine-formula';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SortRangeCommand } from '../../commands/commands/sheets-sort.command';
import { SheetsSortService } from '../sheets-sort.service';
describe('SheetsSortService', () => {
let service: SheetsSortService;
let sheet: any;
let arrayFormulaRange: any;
let executeCommand: ReturnType<typeof vi.fn>;
beforeEach(() => {
const injector = new Injector();
sheet = {
getSheetId: () => 'sheet-1',
getMergeData: () => [],
getCellRaw: () => null,
};
arrayFormulaRange = {};
executeCommand = vi.fn(async () => true);
const workbook = {
getUnitId: () => 'book-1',
getSheetBySheetId: (sheetId: string) => (sheetId === 'sheet-1' ? sheet : null),
getActiveSheet: () => sheet,
};
class TestUniverInstanceService {
getUnit = (unitId: string) => (unitId === 'book-1' ? workbook : null);
getCurrentUnitOfType = () => workbook;
}
class TestCommandService {
executeCommand = executeCommand;
}
class TestFormulaDataModel {
getArrayFormulaRange = () => arrayFormulaRange;
}
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
injector.add([ICommandService, { useClass: TestCommandService as never }]);
injector.add([FormulaDataModel, { useClass: TestFormulaDataModel as never }]);
injector.add([SheetsSortService]);
service = injector.get(SheetsSortService);
});
it('requires multi-row ranges for sort actions', () => {
expect(service.singleCheck({ unitId: 'book-1', subUnitId: 'sheet-1', range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 2 } })).toBe(false);
expect(service.singleCheck({ unitId: 'book-1', subUnitId: 'sheet-1', range: { startRow: 2, endRow: 4, startColumn: 0, endColumn: 2 } })).toBe(true);
});
it('uses the most recently registered comparator first', () => {
const first: ICellValueCompareFn = () => 1;
const second: ICellValueCompareFn = () => -1;
service.registerCompareFn(first);
service.registerCompareFn(second);
expect(service.getAllCompareFns()).toEqual([second, first]);
});
it('validates sort ranges against merge layout and non-empty cells', () => {
const location = { unitId: 'book-1', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 } };
expect(service.mergeCheck(location)).toBe(true);
sheet.getMergeData = () => [{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 }];
expect(service.mergeCheck(location)).toBe(true);
sheet.getMergeData = () => [
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
];
expect(service.mergeCheck(location)).toBe(false);
sheet.getCellRaw = (row: number, col: number) => (row === 1 && col === 1 ? { v: 'value' } : null);
expect(service.emptyCheck(location)).toBe(true);
sheet.getCellRaw = () => null;
expect(service.emptyCheck(location)).toBe(false);
expect(service.mergeCheck({ ...location, unitId: 'missing' })).toBe(false);
expect(service.emptyCheck({ ...location, unitId: 'missing' })).toBe(false);
});
it('rejects sort ranges that intersect array formulas', () => {
const location = { unitId: 'book-1', subUnitId: 'sheet-1', range: { startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 } };
expect(service.formulaCheck(location)).toBe(true);
arrayFormulaRange = {
'book-1': {
'sheet-1': {
1: {
1: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 3 },
},
},
},
};
expect(service.formulaCheck(location)).toBe(false);
});
it('applies sort options through the command service with explicit or active sheet targets', async () => {
service.applySort({
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
orderRules: [{ colIndex: 1, type: 'asc' }] as never,
hasTitle: true,
});
service.applySort({
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
orderRules: [] as never,
}, 'book-2', 'sheet-2');
expect(executeCommand).toHaveBeenNthCalledWith(1, SortRangeCommand.id, {
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
orderRules: [{ colIndex: 1, type: 'asc' }],
hasTitle: true,
unitId: 'book-1',
subUnitId: 'sheet-1',
});
expect(executeCommand).toHaveBeenNthCalledWith(2, SortRangeCommand.id, {
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 },
orderRules: [],
hasTitle: false,
unitId: 'book-2',
subUnitId: 'sheet-2',
});
});
});

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRange } from '@univerjs/core';
export interface ISortOption {
range: IRange;
orderRules: IOrderRule[];
hasTitle?: boolean;
}
export enum SortType {
DESC = 'desc', // Z-A
ASC = 'asc', // A-Z
}
export interface IOrderRule {
type: SortType;
colIndex: number;
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRange, Workbook } from '@univerjs/core';
import type { ISheetRangeLocation } from '@univerjs/sheets';
import type { ICellValueCompareFn } from '../commands/commands/sheets-sort.command';
import type { ISortOption } from './interface';
import {
Disposable,
ICommandService,
Inject,
IUniverInstanceService,
Rectangle,
} from '@univerjs/core';
import { FormulaDataModel } from '@univerjs/engine-formula';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { SortRangeCommand } from '../commands/commands/sheets-sort.command';
import { isNullValue } from '../controllers/utils';
export class SheetsSortService extends Disposable {
private _compareFns: ICellValueCompareFn[] = [];
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
) {
super();
}
mergeCheck(location: ISheetRangeLocation) {
const { unitId, subUnitId, range } = location;
const sheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId);
if (!sheet) {
return false;
}
const mergeDataInRange = sheet.getMergeData().filter((merge) => Rectangle.contains(range, merge));
if (mergeDataInRange.length === 0) {
return true;
}
return isRangeDividedEqually(range, mergeDataInRange);
}
emptyCheck(location: ISheetRangeLocation) {
const { unitId, subUnitId, range } = location;
const sheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId);
if (!sheet) {
return false;
}
for (let row = range.startRow; row <= range.endRow; row++) {
for (let col = range.startColumn; col <= range.endColumn; col++) {
if (!isNullValue(sheet.getCellRaw(row, col))) {
return true;
}
}
}
return false;
}
singleCheck(location: ISheetRangeLocation) {
if (location.range.startRow === location.range.endRow) {
return false;
}
return true;
}
formulaCheck(location: ISheetRangeLocation) {
const { unitId, subUnitId, range } = location;
const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange()?.[unitId]?.[subUnitId];
for (const row in arrayFormulaRange) {
const rowData = arrayFormulaRange[Number(row)];
for (const col in rowData) {
const arrayFormula = rowData[Number(col)];
if (arrayFormula && Rectangle.intersects(range, arrayFormula)) {
return false;
}
}
}
return true;
}
registerCompareFn(fn: ICellValueCompareFn) {
this._compareFns.unshift(fn);
}
getAllCompareFns(): ICellValueCompareFn[] {
return this._compareFns;
}
applySort(sortOption: ISortOption, unitId?: string, subUnitId?: string) {
const { unitId: _unitId, subUnitId: _subUnitId } = getSheetCommandTarget(this._univerInstanceService) || {};
this._commandService.executeCommand(SortRangeCommand.id, {
orderRules: sortOption.orderRules,
range: sortOption.range,
hasTitle: sortOption.hasTitle ?? false,
unitId: unitId || _unitId,
subUnitId: subUnitId || _subUnitId,
});
}
}
function isRangeDividedEqually(range: IRange, merges: IRange[]): boolean {
const rangeRows = range.endRow - range.startRow + 1;
const rangeCols = range.endColumn - range.startColumn + 1;
let mergeRows: number | null = null;
let mergeCols: number | null = null;
const totalArea = rangeRows * rangeCols;
let totalMergeArea = 0;
for (const merge of merges) {
if (merge.startRow >= range.startRow && merge.endRow <= range.endRow && merge.startColumn >= range.startColumn && merge.endColumn <= range.endColumn) {
const currentMergeRows = merge.endRow - merge.startRow + 1;
const currentMergeCols = merge.endColumn - merge.startColumn + 1;
if (mergeRows === null && mergeCols === null) {
mergeRows = currentMergeRows;
mergeCols = currentMergeCols;
} else if (currentMergeRows !== mergeRows || currentMergeCols !== mergeCols) {
return false;
}
totalMergeArea += currentMergeRows * currentMergeCols;
}
}
return totalMergeArea === totalArea;
}

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