diff --git a/examples/package.json b/examples/package.json index 5f55fd5664..d3648f767f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -41,6 +41,8 @@ "@univerjs/sheets-hyper-link": "workspace:*", "@univerjs/sheets-hyper-link-ui": "workspace:*", "@univerjs/sheets-numfmt": "workspace:*", + "@univerjs/sheets-sort": "workspace:*", + "@univerjs/sheets-sort-ui": "workspace:*", "@univerjs/sheets-thread-comment": "workspace:*", "@univerjs/sheets-ui": "workspace:*", "@univerjs/sheets-zen-editor": "workspace:*", diff --git a/examples/src/data/sheets/demo/default-workbook-data-demo.ts b/examples/src/data/sheets/demo/default-workbook-data-demo.ts index 728344a060..d7d2b54ada 100644 --- a/examples/src/data/sheets/demo/default-workbook-data-demo.ts +++ b/examples/src/data/sheets/demo/default-workbook-data-demo.ts @@ -14007,6 +14007,12 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { s: '1', v: 'A Schedule of Items', }, + 10: { + v: 10, + }, + 11: { + v: 234, + }, }, 1: { 0: { @@ -14017,11 +14023,31 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { }, v: 1, }, + 10: { + v: 12313, + }, + 11: { + v: 123, + }, }, 2: { 0: { f: '=A2', }, + 10: { + v: 0, + }, + 11: { + v: 235, + }, + }, + 4: { + 10: { + v: 123, + }, + 11: { + v: 632, + }, }, 5: { 5: { @@ -14040,6 +14066,9 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { 9: { s: 'uJSelZ22', }, + 11: { + v: 126, + }, }, 6: { 5: { @@ -14055,8 +14084,18 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { 8: { s: 'uJSelZ22', }, + 10: { + v: 'ewe', + }, + 11: { + v: 893, + }, + }, + 10: { + 11: { + v: 12, + }, }, - 10: {}, 11: { 4: { v: 123, @@ -14169,7 +14208,13 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = { scrollLeft: 0, defaultColumnWidth: 73, defaultRowHeight: 19, - mergeData: [], + mergeData: [ + { startRow: 0, endRow: 1, startColumn: 10, endColumn: 10 }, + { startRow: 2, endRow: 3, startColumn: 10, endColumn: 10 }, + { startRow: 4, endRow: 5, startColumn: 10, endColumn: 10 }, + { startRow: 6, endRow: 7, startColumn: 10, endColumn: 10 }, + { startRow: 8, endRow: 9, startColumn: 10, endColumn: 10 }, + ], rowData: { 11: { hd: 0, diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index c207767e75..e2e51809cb 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -36,9 +36,10 @@ import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor'; import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation'; import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui'; import { FUniver } from '@univerjs/facade'; +import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; +import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; import { enUS, ruRU, zhCN } from '../locales'; import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo'; - /* eslint-disable-next-line node/prefer-global/process */ const IS_E2E: boolean = !!process.env.IS_E2E; @@ -90,6 +91,10 @@ univer.registerPlugin(UniverSheetsHyperLinkUIPlugin); // data validation univer.registerPlugin(UniverSheetsDataValidationPlugin); +// sort +univer.registerPlugin(UniverSheetsSortPlugin); +univer.registerPlugin(UniverSheetsSortUIPlugin); + // sheet condition formatting univer.registerPlugin(UniverSheetsConditionalFormattingUIPlugin); diff --git a/packages/design/src/components/confirm/Confirm.tsx b/packages/design/src/components/confirm/Confirm.tsx index 183865d42a..0057d03c59 100644 --- a/packages/design/src/components/confirm/Confirm.tsx +++ b/packages/design/src/components/confirm/Confirm.tsx @@ -55,6 +55,12 @@ export interface IConfirmProps { * Callback when the Confirm is confirmed. */ onConfirm?: () => void; + + /** + * The width of the Confirm. + */ + width?: number | string; + } function Footer(props: { locale: ILocale['design']; cancelText?: string; confirmText?: string; onClose: (() => void) | undefined; onConfirm: (() => void) | undefined }) { @@ -71,7 +77,7 @@ function Footer(props: { locale: ILocale['design']; cancelText?: string; confirm } export function Confirm(props: IConfirmProps) { - const { children, visible = false, title, cancelText, confirmText, onClose, onConfirm } = props; + const { children, visible = false, title, cancelText, confirmText, width, onClose, onConfirm } = props; const { locale } = useContext(ConfigContext); @@ -89,6 +95,7 @@ export function Confirm(props: IConfirmProps) { /> )} onClose={onClose} + width={width} > {children} diff --git a/packages/design/src/components/dialog/Dialog.tsx b/packages/design/src/components/dialog/Dialog.tsx index b2f771e23a..a2e894c4f1 100644 --- a/packages/design/src/components/dialog/Dialog.tsx +++ b/packages/design/src/components/dialog/Dialog.tsx @@ -87,6 +87,10 @@ export interface IDialogProps { */ onClose?: () => void; + /** + * Whether the dialog should show a mask. + */ + mask?: boolean; className?: string; } @@ -105,6 +109,7 @@ export function Dialog(props: IDialogProps) { preservePositionOnDestroy = false, footer, onClose, + mask, } = props; const [dragDisabled, setDragDisabled] = useState(false); const [positionOffset, setPositionOffset] = useState<{ x: number; y: number } | null>(null); @@ -184,12 +189,14 @@ export function Dialog(props: IDialogProps) { : modal; }; + const needMask = mask ?? !draggable; + return mountContainer && ( mountContainer} visible={visible} title={TitleIfDraggable} @@ -197,7 +204,7 @@ export function Dialog(props: IDialogProps) { closeIcon={closeIcon} destroyOnClose={destroyOnClose} footer={footer} - mask={!draggable} + mask={needMask} style={style} onClose={onClose} > diff --git a/packages/sheets-conditional-formatting/src/models/conditional-formatting-rule-model.ts b/packages/sheets-conditional-formatting/src/models/conditional-formatting-rule-model.ts index ca0326fd36..3c4c768508 100644 --- a/packages/sheets-conditional-formatting/src/models/conditional-formatting-rule-model.ts +++ b/packages/sheets-conditional-formatting/src/models/conditional-formatting-rule-model.ts @@ -103,9 +103,8 @@ export class ConditionalFormattingRuleModel { const conditionalFormattingService = this._injector.get(ConditionalFormattingService); Object.assign(oldRule, rule); - const dispose = conditionalFormattingService.interceptorManager.intercept(conditionalFormattingService.interceptorManager.getInterceptPoints().beforeUpdateRuleResult, { - handler: (config) => { + handler: (config, _, next) => { if (unitId === config?.unitId && subUnitId === config.subUnitId && oldRule.cfId === config.cfId) { cloneRange.forEach((range) => { Range.foreach(range, (row, col) => { @@ -119,7 +118,9 @@ export class ConditionalFormattingRuleModel { }); }); dispose(); + return; } + next(config); }, }); diff --git a/packages/sheets-conditional-formatting/src/services/conditional-formatting.service.ts b/packages/sheets-conditional-formatting/src/services/conditional-formatting.service.ts index 7ac6882855..7b30bfc775 100644 --- a/packages/sheets-conditional-formatting/src/services/conditional-formatting.service.ts +++ b/packages/sheets-conditional-formatting/src/services/conditional-formatting.service.ts @@ -16,8 +16,8 @@ import type { IMutationInfo, IRange, Workbook } from '@univerjs/core'; import { afterInitApply, createInterceptorKey, Disposable, ICommandService, InterceptorManager, IResourceManagerService, IUniverInstanceService, LifecycleStages, ObjectMatrix, OnLifecycle, Rectangle, Tools, UniverInstanceType } from '@univerjs/core'; -import type { IInsertColMutationParams, IMoveColumnsMutationParams, IMoveRangeMutationParams, IMoveRowsMutationParams, IRemoveRowsMutationParams, IRemoveSheetCommandParams, ISetRangeValuesMutationParams } from '@univerjs/sheets'; -import { InsertColMutation, InsertRowMutation, MoveColsMutation, MoveRangeMutation, MoveRowsMutation, RemoveColMutation, RemoveRowMutation, RemoveSheetCommand, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets'; +import type { IInsertColMutationParams, IMoveColumnsMutationParams, IMoveRangeMutationParams, IMoveRowsMutationParams, IRemoveRowsMutationParams, IRemoveSheetCommandParams, IReorderRangeMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets'; +import { InsertColMutation, InsertRowMutation, MoveColsMutation, MoveRangeMutation, MoveRowsMutation, RemoveColMutation, RemoveRowMutation, RemoveSheetCommand, ReorderRangeMutation, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets'; import { Inject, Injector } from '@wendellhu/redi'; import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -360,6 +360,18 @@ export class ConditionalFormattingService extends Disposable { handleSubUnit(from); break; } + case ReorderRangeMutation.id: { + const { range, unitId, subUnitId } = commandInfo.params as IReorderRangeMutationParams; + const allRules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId); + if (allRules) { + const effectRule = allRules.filter((rule) => rule.ranges.some((ruleRange) => Rectangle.intersects(ruleRange, range))); + effectRule.forEach((rule) => { + this._conditionalFormattingViewModel.markRuleDirty(unitId, subUnitId, rule); + this._deleteComputeCache(unitId, subUnitId, rule.cfId); + }); + } + break; + } } })); } diff --git a/packages/sheets-filter-ui/src/views/components/SheetsFilterPanel.tsx b/packages/sheets-filter-ui/src/views/components/SheetsFilterPanel.tsx index 1a75440d41..5756d6a243 100644 --- a/packages/sheets-filter-ui/src/views/components/SheetsFilterPanel.tsx +++ b/packages/sheets-filter-ui/src/views/components/SheetsFilterPanel.tsx @@ -17,10 +17,12 @@ import React, { useCallback, useMemo } from 'react'; import { Button, type ISegmentedProps, Segmented } from '@univerjs/design'; import { useDependency } from '@wendellhu/redi/react-bindings'; -import { useObservable } from '@univerjs/ui'; +import { ComponentContainer, useComponentsOfPart, useObservable } from '@univerjs/ui'; import { ICommandService, LocaleService } from '@univerjs/core'; import { of } from 'rxjs'; +import { SheetsFilterService } from '@univerjs/sheets-filter'; +import { SheetsUIPart } from '@univerjs/sheets-ui'; import type { ByConditionsModel, ByValuesModel } from '../../services/sheets-filter-panel.service'; import { FilterBy, SheetsFilterPanelService } from '../../services/sheets-filter-panel.service'; import { ChangeFilterByOperation, CloseFilterPanelOperation } from '../../commands/sheets-filter.operation'; @@ -64,8 +66,17 @@ export function FilterPanel() { commandService.executeCommand(CloseFilterPanelOperation.id); }, [filterByModel, commandService]); + const filterService = useDependency(SheetsFilterService); + const range = filterService.activeFilterModel?.getRange(); + const colIndex = sheetsFilterPanelService.col; + const FilterPanelEmbedPointPart = useComponentsOfPart(SheetsUIPart.FILTER_PANEL_EMBED_POINT); + return (
+
onFilterByTypeChange(value as FilterBy)}>
@@ -98,3 +109,4 @@ function useFilterByOptions(localeService: LocaleService): ISegmentedProps['opti // eslint-disable-next-line react-hooks/exhaustive-deps , [locale, localeService]); } + diff --git a/packages/sheets-filter-ui/src/views/components/index.module.less b/packages/sheets-filter-ui/src/views/components/index.module.less index dd10ae8e72..791dbbce39 100644 --- a/packages/sheets-filter-ui/src/views/components/index.module.less +++ b/packages/sheets-filter-ui/src/views/components/index.module.less @@ -1,7 +1,7 @@ .sheets-filter-panel { display: flex; // width: 312px; - height: 400px; + height: 432px; flex-direction: column; background: rgb(var(--bg-color-secondary)); border-radius: 10px; diff --git a/packages/sheets-filter/src/controllers/sheets-filter.controller.ts b/packages/sheets-filter/src/controllers/sheets-filter.controller.ts index 6adcf064b7..0bcd483c86 100644 --- a/packages/sheets-filter/src/controllers/sheets-filter.controller.ts +++ b/packages/sheets-filter/src/controllers/sheets-filter.controller.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import type { ICommandInfo, IMutationInfo, IObjectArrayPrimitiveType, Nullable } from '@univerjs/core'; +import type { ICommandInfo, IMutationInfo, IObjectArrayPrimitiveType, IRange, Nullable, Workbook } from '@univerjs/core'; import { Disposable, DisposableCollection, ICommandService, IUniverInstanceService, LifecycleStages, moveMatrixArray, OnLifecycle, Rectangle } from '@univerjs/core'; -import type { EffectRefRangeParams, IAddWorksheetMergeMutationParams, IInsertColCommandParams, IInsertRowCommandParams, IInsertRowMutationParams, IMoveColsCommandParams, IMoveRangeCommandParams, IMoveRowsCommandParams, IRemoveColMutationParams, IRemoveRowsMutationParams, IRemoveSheetCommandParams, ISetWorksheetActivateCommandParams, ISheetCommandSharedParams } from '@univerjs/sheets'; -import { EffectRefRangId, getSheetCommandTarget, InsertColCommand, InsertRowCommand, InsertRowMutation, INTERCEPTOR_POINT, MoveRangeCommand, MoveRowsCommand, RefRangeService, RemoveColCommand, RemoveRowCommand, RemoveRowMutation, RemoveSheetCommand, SetWorksheetActivateCommand, SheetInterceptorService } from '@univerjs/sheets'; +import type { EffectRefRangeParams, IAddWorksheetMergeMutationParams, IInsertColCommandParams, IInsertRowCommandParams, IInsertRowMutationParams, IMoveColsCommandParams, IMoveRangeCommandParams, IMoveRowsCommandParams, IRemoveColMutationParams, IRemoveRowsMutationParams, IRemoveSheetCommandParams, ISetRangeValuesMutationParams, ISetWorksheetActivateCommandParams, ISheetCommandSharedParams } from '@univerjs/sheets'; +import { EffectRefRangId, expandToContinuousRange, getSheetCommandTarget, InsertColCommand, InsertRowCommand, InsertRowMutation, INTERCEPTOR_POINT, MoveRangeCommand, MoveRowsCommand, RefRangeService, RemoveColCommand, RemoveRowCommand, RemoveRowMutation, RemoveSheetCommand, SetRangeValuesMutation, SetWorksheetActivateCommand, SheetInterceptorService } from '@univerjs/sheets'; import { Inject } from '@wendellhu/redi'; import { SheetsFilterService } from '../services/sheet-filter.service'; @@ -694,7 +694,7 @@ export class SheetsFilterController extends Disposable { } private _commandExecutedListener() { - this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo) => { + this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options) => { const { unitId, subUnitId } = command.params as unknown as ISheetCommandSharedParams || {}; const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId); @@ -737,60 +737,61 @@ export class SheetsFilterController extends Disposable { filterModel.filteredOutRows = new Set(newFilteredOutRows); } - // if (command.id === DeleteRangeMoveLeftCommand.id || command.id === DeleteRangeMoveUpCommand.id || command.id === InsertRangeMoveRightCommand.id || command.id === InsertRangeMoveDownCommand.id) { - // const { range } = command.params as (IDeleteRangeMoveUpCommandParams | IDeleteRangeMoveLeftCommandParams | InsertRangeMoveDownCommandParams | InsertRangeMoveRightCommandParams); - // const { startRow } = range; - // const { endRow: filterEndRow } = filterModel.getRange(); - // if (startRow <= filterEndRow) { - // filterModel.reCalc(); - // } - // } - - // InsertRowsOrCols / RemoveRowsOrCols Mutations - // if (mutationIdByRowCol.includes(command.id)) { - // const params = command.params as IInsertRowCommandParams; - // if (!params) return; - // const { range } = params; - - // const isRowOperation = command.id.includes('row'); - // const isAddOperation = command.id.includes('insert'); - - // const operationStart = isRowOperation ? range.startRow : range.startColumn; - // const operationEnd = isRowOperation ? range.endRow : range.endColumn; - // const operationCount = operationEnd - operationStart + 1; - - // let { startRow, endRow, startColumn, endColumn } = filterModel.getRange(); - - // if (isAddOperation) { - // if (isRowOperation) { - // if (operationStart <= startRow) { - // startRow += operationCount; - // endRow += operationCount; - // } - // } else { - // if (operationStart <= startColumn) { - // startColumn += operationCount; - // endColumn += operationCount; - // } - // } - // } else { - // if (isRowOperation) { - // if (operationEnd < startRow) { - // startRow -= operationCount; - // endRow -= operationCount; - // } - // } else { - // if (operationEnd < startColumn) { - // startColumn -= operationCount; - // endColumn -= operationCount; - // } - // } - // } - // filterModel.setRange({ startRow, endRow, startColumn, endColumn }); - // } + // extend filter range when set range values + if (command.id === SetRangeValuesMutation.id && !options?.fromCollab && !options?.onlyLocal) { + const extendRegion = this._getExtendRegion(unitId, subUnitId); + if (extendRegion) { + const cellValue = (command.params as ISetRangeValuesMutationParams).cellValue; + if (cellValue) { + for (let col = extendRegion.startColumn; col <= extendRegion.endColumn; col++) { + const cell = cellValue?.[extendRegion.startRow]?.[col]; + if (cell && Object.keys(cell).length !== 0) { + const worksheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId); + if (worksheet) { + const extendedRange = expandToContinuousRange(extendRegion, { down: true }, worksheet); + const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId)!; + const filterRange = filterModel.getRange(); + filterModel.setRange({ + ...filterRange, + endRow: extendedRange.endRow, + }); + } + } + } + } + } + } })); } + private _getExtendRegion(unitId: string, subUnitId: string): Nullable { + const filterModel = this._sheetsFilterService.getFilterModel(unitId, subUnitId); + if (!filterModel) { + return null; + } + const worksheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId); + if (!worksheet) { + return null; + } + const filterRange = filterModel.getRange(); + if (!filterRange) { + return null; + } + const maxRowIndex = worksheet.getRowCount() - 1; + const rowManager = worksheet.getRowManager(); + for (let row = filterRange.endRow + 1; row <= maxRowIndex; row++) { + if (rowManager.getRowRawVisible(row)) { + return { + startRow: row, + endRow: row, + startColumn: filterRange.startColumn, + endColumn: filterRange.endColumn, + }; + } + } + return null; + } + private _initErrorHandling() { this.disposeWithMe(this._commandService.beforeCommandExecuted((command) => { const params = command.params as IMoveRowsCommandParams; diff --git a/packages/sheets-formula/src/controllers/__tests__/formula-auto-fill.controller.spec.ts b/packages/sheets-formula/src/controllers/__tests__/formula-auto-fill.controller.spec.ts index 03c0d250fa..c944a660d8 100644 --- a/packages/sheets-formula/src/controllers/__tests__/formula-auto-fill.controller.spec.ts +++ b/packages/sheets-formula/src/controllers/__tests__/formula-auto-fill.controller.spec.ts @@ -41,6 +41,7 @@ import { IAutoFillService, ISelectionRenderService, SelectionRenderService, + SheetsRenderService, } from '@univerjs/sheets-ui'; import { DesktopPlatformService, DesktopShortcutService, IPlatformService, IShortcutService } from '@univerjs/ui'; import type { Injector } from '@wendellhu/redi'; @@ -53,6 +54,11 @@ const theme = { colorBlack: '#35322b', }; +class mockSheetsRenderService { + registerSkeletonChangingMutations(id: string) { + } +} + describe('Test auto fill with formula', () => { let univer: Univer; let get: Injector['get']; @@ -77,6 +83,7 @@ describe('Test auto fill with formula', () => { [RangeProtectionRuleModel], [RangeProtectionService], [RangeProtectionRenderModel], + [SheetsRenderService, { useClass: mockSheetsRenderService }], ]); univer = testBed.univer; get = testBed.get; diff --git a/packages/sheets-formula/src/controllers/active-dirty.controller.ts b/packages/sheets-formula/src/controllers/active-dirty.controller.ts index 5ff4b96ac9..39def9f38f 100644 --- a/packages/sheets-formula/src/controllers/active-dirty.controller.ts +++ b/packages/sheets-formula/src/controllers/active-dirty.controller.ts @@ -34,6 +34,7 @@ import type { IRemoveColMutationParams, IRemoveRowsMutationParams, IRemoveSheetMutationParams, + IReorderRangeMutationParams, ISetDefinedNameCommandParams, ISetRangeValuesMutationParams, } from '@univerjs/sheets'; @@ -45,6 +46,7 @@ import { RemoveColMutation, RemoveRowMutation, RemoveSheetMutation, + ReorderRangeMutation, SetDefinedNameCommand, SetRangeValuesMutation, SetStyleCommand, @@ -152,6 +154,16 @@ export class ActiveDirtyController extends Disposable { }; }, }); + + this._activeDirtyManagerService.register(ReorderRangeMutation.id, { + commandId: ReorderRangeMutation.id, + getDirtyData: (command: ICommandInfo) => { + const params = command.params as IReorderRangeMutationParams; + return { + dirtyRanges: this._getReorderRangeMutationDirtyRange(params), + }; + }, + }); } private _initialRowAndColumn() { @@ -285,6 +297,15 @@ export class ActiveDirtyController extends Disposable { return dirtyRanges; } + private _getReorderRangeMutationDirtyRange(params: IReorderRangeMutationParams) { + const { unitId, subUnitId: sheetId, range } = params; + const matrix = this._rangeToMatrix(range).getData(); + const dirtyRanges: IUnitRange[] = []; + dirtyRanges.push(...this._getDirtyRangesByCellValue(unitId, sheetId, matrix)); + dirtyRanges.push(...this._getDirtyRangesForArrayFormula(unitId, sheetId, matrix)); + return dirtyRanges; + } + private _getDeleteRangeMutationDirtyRange(params: IDeleteRangeMutationParams) { const { subUnitId: sheetId, unitId, range, shiftDimension } = params; diff --git a/packages/sheets-sort-ui/README-zh.md b/packages/sheets-sort-ui/README-zh.md new file mode 100644 index 0000000000..986c8c1acb --- /dev/null +++ b/packages/sheets-sort-ui/README-zh.md @@ -0,0 +1,34 @@ +# @univerjs/sheets-sort-ui + +[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-sort-ui)](https://npmjs.org/packages/@univerjs/sheets-sort-ui) +[![license](https://img.shields.io/npm/l/@univerjs/sheets-sort-ui)](https://img.shields.io/npm/l/@univerjs/sheets-sort-ui) +![CSS Included](https://img.shields.io/badge/CSS_Included-blue?logo=CSS3) +![i18n](https://img.shields.io/badge/zh--CN%20%7C%20en--US-cornflowerblue?label=i18n) + +## 简介 + +`@univerjs/sheets-sort-ui` 为 sheets 提供了排序功能的用户界面。 + +## 使用指南 + +你应该将此插件与 `@univerjs/sheets-sort` 包一起使用。 + +```ts +import '@univerjs/sheets-sort-ui/lib/index.css'; + +import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; +import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; + +univer.registerPlugin(UniverSheetsSortPlugin); +univer.registerPlugin(UniverSheetsSortUIPlugin); +``` + +### 安装 + +```shell +# 使用 npm +npm install @univerjs/sheets-sort-ui + +# 使用 pnpm +pnpm add @univerjs/sheets-sort-ui +``` diff --git a/packages/sheets-sort-ui/README.md b/packages/sheets-sort-ui/README.md new file mode 100644 index 0000000000..d56e6bd807 --- /dev/null +++ b/packages/sheets-sort-ui/README.md @@ -0,0 +1,34 @@ +# @univerjs/sheets-sort-ui + +[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-sort-ui)](https://npmjs.org/packages/@univerjs/sheets-sort-ui) +[![license](https://img.shields.io/npm/l/@univerjs/sheets-sort-ui)](https://img.shields.io/npm/l/@univerjs/sheets-sort-ui) +![CSS Included](https://img.shields.io/badge/CSS_Included-blue?logo=CSS3) +![i18n](https://img.shields.io/badge/zh--CN%20%7C%20en--US-cornflowerblue?label=i18n) + +## Introduction + +`@univerjs/sheets-sort-ui` provides a user interface for sorting in sheets. + +## Usage + +You should use this plugin with the `@univerjs/sheets-sort` package. + +```ts +import '@univerjs/sheets-sort-ui/lib/index.css'; + +import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; +import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; + +univer.registerPlugin(UniverSheetsSortPlugin); +univer.registerPlugin(UniverSheetsSortUIPlugin); +``` + +### Installation + +```shell +# Using npm +npm install @univerjs/sheets-sort-ui + +# Using pnpm +pnpm add @univerjs/sheets-sort-ui +``` diff --git a/packages/sheets-sort-ui/package.json b/packages/sheets-sort-ui/package.json new file mode 100644 index 0000000000..abe58aa8e4 --- /dev/null +++ b/packages/sheets-sort-ui/package.json @@ -0,0 +1,99 @@ +{ + "name": "@univerjs/sheets-sort-ui", + "version": "0.1.10", + "private": false, + "description": "Univer Sheets Sort UI", + "author": "DreamNum ", + "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": [], + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*", + "./locale/*": "./src/locale/*.ts" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/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" + }, + "./lib/*": "./lib/*" + } + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "lint:types": "tsc --noEmit", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-formula": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@univerjs/sheets-sort": "workspace:*", + "@univerjs/sheets-ui": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": "^0.15.2", + "react": "18.3.1", + "react-dom": "18.3.1", + "rxjs": ">=7.0.0" + }, + "dependencies": { + "@univerjs/icons": "^0.1.56", + "rc-virtual-list": "^3.11.5" + }, + "devDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/shared": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@univerjs/sheets-sort": "workspace:*", + "@univerjs/sheets-ui": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": "^0.15.2", + "clsx": "^2.1.1", + "less": "^4.2.0", + "react": "18.3.1", + "rxjs": "^7.8.1", + "typescript": "^5.4.5", + "vite": "^5.2.10", + "vitest": "^1.5.2" + } +} diff --git a/packages/sheets-sort-ui/src/commands/sheets-sort.command.ts b/packages/sheets-sort-ui/src/commands/sheets-sort.command.ts new file mode 100644 index 0000000000..b5bd36a0c6 --- /dev/null +++ b/packages/sheets-sort-ui/src/commands/sheets-sort.command.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandType, type ICommand } from '@univerjs/core'; +import type { IAccessor } from '@wendellhu/redi'; +import { SheetsSortUIService } from './../services/sheets-sort-ui.service'; + +export const SortRangeAscCommand: ICommand = { + id: 'sheet.command.sort-range-asc', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(true, false); + }, +}; + +export const SortRangeAscExtCommand: ICommand = { + id: 'sheet.command.sort-range-asc-ext', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(true, true); + }, +}; + +export const SortRangeDescCommand: ICommand = { + id: 'sheet.command.sort-range-desc', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(false, false); + }, +}; + +export const SortRangeDescExtCommand: ICommand = { + id: 'sheet.command.sort-range-desc-ext', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(false, true); + }, +}; + +export const SortRangeCustomCommand: ICommand = { + id: 'sheet.command.sort-range-custom', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortCustomize(); + }, +}; + +export const SortRangeAscInCtxMenuCommand: ICommand = { + id: 'sheet.command.sort-range-asc-ctx', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(true, false); + }, +}; + +export const SortRangeAscExtInCtxMenuCommand: ICommand = { + id: 'sheet.command.sort-range-asc-ext-ctx', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(true, true); + }, +}; + +export const SortRangeDescInCtxMenuCommand: ICommand = { + id: 'sheet.command.sort-range-desc-ctx', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(false, false); + }, +}; + +export const SortRangeDescExtInCtxMenuCommand: ICommand = { + id: 'sheet.command.sort-range-desc-ext-ctx', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortDirectly(false, true); + }, +}; + +export const SortRangeCustomInCtxMenuCommand: ICommand = { + id: 'sheet.command.sort-range-custom-ctx', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor) => { + const sortService = accessor.get(SheetsSortUIService); + return await sortService.triggerSortCustomize(); + }, +}; diff --git a/packages/sheets-sort-ui/src/controllers/sheets-sort-ui.controller.ts b/packages/sheets-sort-ui/src/controllers/sheets-sort-ui.controller.ts new file mode 100644 index 0000000000..ca83b1f5b9 --- /dev/null +++ b/packages/sheets-sort-ui/src/controllers/sheets-sort-ui.controller.ts @@ -0,0 +1,152 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniverInstanceService } from '@univerjs/core'; +import { ICommandService, IUniverInstanceService, LifecycleStages, LocaleService, OnLifecycle, RxDisposable } from '@univerjs/core'; + +import { Inject, Injector } from '@wendellhu/redi'; +import type { MenuConfig, UIPartsService } from '@univerjs/ui'; +import { ComponentManager, IDialogService, ILayoutService, IMenuService, IUIPartsService } from '@univerjs/ui'; +import { takeUntil } from 'rxjs'; +import { serializeRange } from '@univerjs/engine-formula'; +import { AscendingSingle, CustomSortSingle, DescendingSingle, ExpandAscendingSingle, ExpandDescendingSingle } from '@univerjs/icons'; +import { connectInjector } from '@wendellhu/redi/react-bindings'; +import { SheetsUIPart } from '@univerjs/sheets-ui'; +import { SortRangeAscCommand, SortRangeAscExtCommand, SortRangeAscExtInCtxMenuCommand, SortRangeAscInCtxMenuCommand, SortRangeCustomCommand, SortRangeCustomInCtxMenuCommand, SortRangeDescCommand, SortRangeDescExtCommand, SortRangeDescExtInCtxMenuCommand, SortRangeDescInCtxMenuCommand } from '../commands/sheets-sort.command'; +import { CustomSortPanel } from '../views/CustomSortPanel'; +import type { ISheetSortLocation } from '../services/sheets-sort-ui.service'; +import { SheetsSortUIService } from '../services/sheets-sort-ui.service'; +import EmbedSortBtn from '../views/EmbedSortBtn'; +import { SHEETS_SORT_ASC_EXT_ICON, SHEETS_SORT_ASC_ICON, SHEETS_SORT_CUSTOM_ICON, SHEETS_SORT_DESC_EXT_ICON, SHEETS_SORT_DESC_ICON, sortRangeAscCtxMenuFactory, sortRangeAscExtCtxMenuFactory, sortRangeAscExtMenuFactory, sortRangeAscMenuFactory, sortRangeCtxMenuFactory, sortRangeCustomCtxMenuFactory, sortRangeCustomMenuFactory, sortRangeDescCtxMenuFactory, sortRangeDescExtCtxMenuFactory, sortRangeDescExtMenuFactory, sortRangeDescMenuFactory, sortRangeMenuFactory } from './sheets-sort.menu'; + +export interface IUniverSheetsSortUIConfig { + menu: MenuConfig; +} +export const DefaultSheetsSortUIConfig = { + menu: {}, +}; + +const CUSTOM_SORT_DIALOG_ID = 'custom-sort-dialog'; +const CUSTOM_SORT_PANEL_WIDTH = 560; + +@OnLifecycle(LifecycleStages.Ready, SheetsSortUIController) +export class SheetsSortUIController extends RxDisposable { + constructor( + private readonly _config: Partial, + @ICommandService private readonly _commandService: ICommandService, + @IUniverInstanceService private readonly _instanceService: UniverInstanceService, + @IMenuService private readonly _menuService: IMenuService, + @IDialogService private readonly _dialogService: IDialogService, + @ILayoutService private readonly _layoutService: ILayoutService, + @IUIPartsService private readonly _uiPartsService: UIPartsService, + @Inject(LocaleService) private readonly _localeService: LocaleService, + @Inject(SheetsSortUIService) private readonly _sheetsSortUIService: SheetsSortUIService, + @Inject(Injector) private _injector: Injector, + @Inject(ComponentManager) private readonly _componentManager: ComponentManager + ) { + super(); + this._initCommands(); + this._initMenu(); + this._initUI(); + } + + private _initMenu() { + const { menu = {} } = this._config; + [ + sortRangeMenuFactory, + sortRangeAscMenuFactory, + sortRangeDescMenuFactory, + sortRangeAscExtMenuFactory, + sortRangeDescExtMenuFactory, + sortRangeCustomMenuFactory, + sortRangeCtxMenuFactory, + sortRangeAscCtxMenuFactory, + sortRangeDescCtxMenuFactory, + sortRangeAscExtCtxMenuFactory, + sortRangeDescExtCtxMenuFactory, + sortRangeCustomCtxMenuFactory, + ].forEach((factory) => { + this.disposeWithMe( + this._menuService.addMenuItem( + this._injector.invoke(factory), menu + ) + ); + }); + } + + private _initCommands(): void { + [ + SortRangeAscCommand, + SortRangeAscExtCommand, + SortRangeDescCommand, + SortRangeDescExtCommand, + SortRangeCustomCommand, + SortRangeAscInCtxMenuCommand, + SortRangeAscExtInCtxMenuCommand, + SortRangeDescInCtxMenuCommand, + SortRangeDescExtInCtxMenuCommand, + SortRangeCustomInCtxMenuCommand, + + ].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); + } + + private _initUI(): void { + this.disposeWithMe(this._componentManager.register('CustomSortPanel', CustomSortPanel)); + this.disposeWithMe(this._uiPartsService.registerComponent(SheetsUIPart.FILTER_PANEL_EMBED_POINT, () => connectInjector(EmbedSortBtn, this._injector))); + this.disposeWithMe(this._componentManager.register(SHEETS_SORT_ASC_ICON, AscendingSingle)); + this.disposeWithMe(this._componentManager.register(SHEETS_SORT_ASC_EXT_ICON, ExpandAscendingSingle)); + this.disposeWithMe(this._componentManager.register(SHEETS_SORT_DESC_ICON, DescendingSingle)); + this.disposeWithMe(this._componentManager.register(SHEETS_SORT_DESC_EXT_ICON, ExpandDescendingSingle)); + this.disposeWithMe(this._componentManager.register(SHEETS_SORT_CUSTOM_ICON, CustomSortSingle)); + + // this controller is also responsible for toggling the CustomSortDialog + this._sheetsSortUIService.customSortState$.pipe(takeUntil(this.dispose$)).subscribe((newState) => { + if (newState && newState.show && newState.location) { + this._openCustomSortPanel(newState.location); + } else if (newState && !newState?.show) { + this._closePanel(); + } + }); + } + + private _openCustomSortPanel(location: ISheetSortLocation): void { + this._dialogService.open({ + id: CUSTOM_SORT_DIALOG_ID, + draggable: true, + width: CUSTOM_SORT_PANEL_WIDTH, + title: { title: `${this._localeService.t('sheets-sort.general.sort-custom')}: ${serializeRange(location.range)}` }, + children: { label: 'CustomSortPanel' }, + destroyOnClose: true, + defaultPosition: getCustomSortDialogDefaultPosition(), + preservePositionOnDestroy: false, + onClose: () => this._closePanel(), + mask: true, + }); + } + + private _closePanel(): void { + this._dialogService.close(CUSTOM_SORT_DIALOG_ID); + + queueMicrotask(() => this._layoutService.focus()); + } +} + +function getCustomSortDialogDefaultPosition(): { x: number; y: number } { + const x = 0; + const y = 0; + + return { x, y }; +} diff --git a/packages/sheets-sort-ui/src/controllers/sheets-sort.menu.ts b/packages/sheets-sort-ui/src/controllers/sheets-sort.menu.ts new file mode 100644 index 0000000000..dbac02c724 --- /dev/null +++ b/packages/sheets-sort-ui/src/controllers/sheets-sort.menu.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UniverInstanceType } from '@univerjs/core'; +import { getMenuHiddenObservable, type IMenuItem, MenuGroup, MenuItemType, MenuPosition } from '@univerjs/ui'; +import type { IAccessor } from '@wendellhu/redi'; +import { getCurrentRangeDisable$, SheetMenuPosition } from '@univerjs/sheets-ui'; +import { RangeProtectionPermissionEditPoint, WorkbookEditablePermission, WorksheetEditPermission, WorksheetSortPermission } from '@univerjs/sheets'; +import { SortRangeAscCommand, SortRangeAscExtCommand, SortRangeAscExtInCtxMenuCommand, SortRangeAscInCtxMenuCommand, SortRangeCustomCommand, SortRangeCustomInCtxMenuCommand, SortRangeDescCommand, SortRangeDescExtCommand, SortRangeDescExtInCtxMenuCommand, SortRangeDescInCtxMenuCommand } from '../commands/sheets-sort.command'; + +const SHEETS_SORT_MENU_ID = 'sheet.menu.sheets-sort'; +const SHEETS_SORT_CTX_MENU_ID = 'sheet.menu.sheets-sort-ctx'; + +export const SHEETS_SORT_ASC_ICON = 'AscendingSingle'; +export const SHEETS_SORT_ASC_EXT_ICON = 'ExpandAscendingSingle'; +export const SHEETS_SORT_DESC_ICON = 'DescendingSingle'; +export const SHEETS_SORT_DESC_EXT_ICON = 'ExpandDescendingSingle'; +export const SHEETS_SORT_CUSTOM_ICON = 'CustomSortSingle'; +export function sortRangeMenuFactory(accessor: IAccessor): IMenuItem { + return { + id: SHEETS_SORT_MENU_ID, + type: MenuItemType.SUBITEMS, + positions: [MenuPosition.TOOLBAR_START], + group: MenuGroup.TOOLBAR_FORMULAS_INSERT, + icon: SHEETS_SORT_ASC_ICON, + tooltip: 'sheets-sort.general.sort', + hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET), + disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + +export function sortRangeAscMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeAscCommand.id, + icon: SHEETS_SORT_ASC_ICON, + title: 'sheets-sort.general.sort-asc-cur', + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_MENU_ID], + }; +} + +export function sortRangeAscExtMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeAscExtCommand.id, + title: 'sheets-sort.general.sort-asc-ext', + icon: SHEETS_SORT_ASC_EXT_ICON, + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_MENU_ID], + }; +} + +export function sortRangeDescMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeDescCommand.id, + title: 'sheets-sort.general.sort-desc-cur', + icon: SHEETS_SORT_DESC_ICON, + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_MENU_ID], + }; +} + +export function sortRangeDescExtMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeDescExtCommand.id, + title: 'sheets-sort.general.sort-desc-ext', + icon: SHEETS_SORT_DESC_EXT_ICON, + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_MENU_ID], + }; +} + +export function sortRangeCustomMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeCustomCommand.id, + title: 'sheets-sort.general.sort-custom', + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_MENU_ID], + icon: SHEETS_SORT_CUSTOM_ICON, + }; +} + +export function sortRangeCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SHEETS_SORT_CTX_MENU_ID, + title: 'sheets-sort.general.sort', + type: MenuItemType.SUBITEMS, + positions: [ + MenuPosition.CONTEXT_MENU, + SheetMenuPosition.COL_HEADER_CONTEXT_MENU, + SheetMenuPosition.ROW_HEADER_CONTEXT_MENU, + ], + group: MenuGroup.CONTEXT_MENU_DATA, + icon: SHEETS_SORT_ASC_ICON, + }; +} + +export function sortRangeAscCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeAscInCtxMenuCommand.id, + title: 'sheets-sort.general.sort-asc-cur', + type: MenuItemType.BUTTON, + icon: SHEETS_SORT_ASC_ICON, + positions: [SHEETS_SORT_CTX_MENU_ID], + disabled$: getCurrentRangeDisable$(_accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + +export function sortRangeAscExtCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeAscExtInCtxMenuCommand.id, + title: 'sheets-sort.general.sort-asc-ext', + type: MenuItemType.BUTTON, + icon: SHEETS_SORT_ASC_EXT_ICON, + positions: [SHEETS_SORT_CTX_MENU_ID], + disabled$: getCurrentRangeDisable$(_accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + +export function sortRangeDescCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeDescInCtxMenuCommand.id, + title: 'sheets-sort.general.sort-desc-cur', + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_CTX_MENU_ID], + icon: SHEETS_SORT_DESC_ICON, + disabled$: getCurrentRangeDisable$(_accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + +export function sortRangeDescExtCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeDescExtInCtxMenuCommand.id, + title: 'sheets-sort.general.sort-desc-ext', + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_CTX_MENU_ID], + icon: SHEETS_SORT_DESC_EXT_ICON, + disabled$: getCurrentRangeDisable$(_accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + +export function sortRangeCustomCtxMenuFactory(_accessor: IAccessor): IMenuItem { + return { + id: SortRangeCustomInCtxMenuCommand.id, + title: 'sheets-sort.general.sort-custom', + type: MenuItemType.BUTTON, + positions: [SHEETS_SORT_CTX_MENU_ID], + icon: SHEETS_SORT_CUSTOM_ICON, + disabled$: getCurrentRangeDisable$(_accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetSortPermission, WorksheetEditPermission], rangeTypes: [RangeProtectionPermissionEditPoint] }), + }; +} + diff --git a/packages/sheets-sort-ui/src/controllers/sheets-sort.shortcut.ts b/packages/sheets-sort-ui/src/controllers/sheets-sort.shortcut.ts new file mode 100644 index 0000000000..b47436ca7a --- /dev/null +++ b/packages/sheets-sort-ui/src/controllers/sheets-sort.shortcut.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO:@yuhongz maybe we should add shortcut for sorting diff --git a/packages/sheets-sort-ui/src/index.ts b/packages/sheets-sort-ui/src/index.ts new file mode 100644 index 0000000000..d1fbe4d1d9 --- /dev/null +++ b/packages/sheets-sort-ui/src/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { UniverSheetsSortUIPlugin } from './plugin'; +export { default as enUS } from './locale/en-US'; +export { default as zhCN } from './locale/zh-CN'; diff --git a/packages/sheets-sort-ui/src/locale/en-US.ts b/packages/sheets-sort-ui/src/locale/en-US.ts new file mode 100644 index 0000000000..8236303fad --- /dev/null +++ b/packages/sheets-sort-ui/src/locale/en-US.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 default { + 'sheets-sort': { + general: { + sort: 'Sort', + 'sort-asc': 'Ascending', + 'sort-desc': 'Descending', + 'sort-custom': 'Custom Sort', + 'sort-asc-ext': 'Expand Ascending', + 'sort-desc-ext': 'Expand Descending', + 'sort-asc-cur': 'Ascending', + 'sort-desc-cur': 'Descending', + }, + error: { + 'merge-size': 'The selected range contains merged cells of different sizes, which cannot be sorted.', + empty: 'The selected range has no content and cannot be sorted.', + single: 'The selected range has only one row and cannot be sorted.', + 'formula-array': 'The selected range has array formulas and cannot be sorted.', + }, + dialog: { + 'sort-reminder': 'Sort Reminder', + 'sort-reminder-desc': 'Extend range sorting or keep range sorting?', + 'sort-reminder-ext': 'Extend range sorting', + 'sort-reminder-no': 'Keep range sorting', + 'first-row-check': 'First row participates in sorting', + 'add-condition': 'Add condition', + cancel: 'Cancel', + confirm: 'Confirm', + }, + }, +}; diff --git a/packages/sheets-sort-ui/src/locale/index.ts b/packages/sheets-sort-ui/src/locale/index.ts new file mode 100644 index 0000000000..1eff5fc204 --- /dev/null +++ b/packages/sheets-sort-ui/src/locale/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { default as enUS } from './en-US'; +export { default as zhCN } from './zh-CN'; diff --git a/packages/sheets-sort-ui/src/locale/ru-RU.ts b/packages/sheets-sort-ui/src/locale/ru-RU.ts new file mode 100644 index 0000000000..8236303fad --- /dev/null +++ b/packages/sheets-sort-ui/src/locale/ru-RU.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 default { + 'sheets-sort': { + general: { + sort: 'Sort', + 'sort-asc': 'Ascending', + 'sort-desc': 'Descending', + 'sort-custom': 'Custom Sort', + 'sort-asc-ext': 'Expand Ascending', + 'sort-desc-ext': 'Expand Descending', + 'sort-asc-cur': 'Ascending', + 'sort-desc-cur': 'Descending', + }, + error: { + 'merge-size': 'The selected range contains merged cells of different sizes, which cannot be sorted.', + empty: 'The selected range has no content and cannot be sorted.', + single: 'The selected range has only one row and cannot be sorted.', + 'formula-array': 'The selected range has array formulas and cannot be sorted.', + }, + dialog: { + 'sort-reminder': 'Sort Reminder', + 'sort-reminder-desc': 'Extend range sorting or keep range sorting?', + 'sort-reminder-ext': 'Extend range sorting', + 'sort-reminder-no': 'Keep range sorting', + 'first-row-check': 'First row participates in sorting', + 'add-condition': 'Add condition', + cancel: 'Cancel', + confirm: 'Confirm', + }, + }, +}; diff --git a/packages/sheets-sort-ui/src/locale/zh-CN.ts b/packages/sheets-sort-ui/src/locale/zh-CN.ts new file mode 100644 index 0000000000..5fda2b4cff --- /dev/null +++ b/packages/sheets-sort-ui/src/locale/zh-CN.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 zLocale: typeof enUS = { + 'sheets-sort': { + general: { + sort: '排序', + 'sort-asc': '升序', + 'sort-desc': '降序', + 'sort-custom': '自定义排序', + 'sort-asc-ext': '拓展区域升序', + 'sort-desc-ext': '拓展区域降序', + 'sort-asc-cur': '当前区域升序', + 'sort-desc-cur': '当前区域降序', + }, + error: { + 'merge-size': '所选区域的合并单元格的大小不一致,无法排序。', + empty: '所选区域无内容,无法排序。', + single: '所选区域仅有一行,无法排序。', + 'formula-array': '所选区域含数组公式,无法排序。', + }, + dialog: { + 'sort-reminder': '排序提醒', + 'sort-reminder-desc': '当前仅会对选中区域进行排序,是否拓展排序范围?', + 'sort-reminder-ext': '拓展排序范围', + 'sort-reminder-no': '保持所选排序范围', + 'first-row-check': '标题不参与排序', + 'add-condition': '添加排序条件', + cancel: '取消', + confirm: '确认', + }, + }, +}; + +export default zLocale; diff --git a/packages/sheets-sort-ui/src/plugin.ts b/packages/sheets-sort-ui/src/plugin.ts new file mode 100644 index 0000000000..d537705248 --- /dev/null +++ b/packages/sheets-sort-ui/src/plugin.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DependentOn, LocaleService, Plugin, Tools, UniverInstanceType } from '@univerjs/core'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; + +import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; +import { enUS, zhCN } from './locale'; +import { SheetsSortUIService } from './services/sheets-sort-ui.service'; +import type { IUniverSheetsSortUIConfig } from './controllers/sheets-sort-ui.controller'; +import { DefaultSheetsSortUIConfig, SheetsSortUIController } from './controllers/sheets-sort-ui.controller'; + +const NAME = 'UNIVER_SHEETS_SORT_UI_PLUGIN'; +@DependentOn(UniverSheetsSortPlugin) +export class UniverSheetsSortUIPlugin extends Plugin { + static override type = UniverInstanceType.UNIVER_SHEET; + static override pluginName = NAME; + + constructor( + private readonly _config: Partial = {}, + @Inject(Injector) protected readonly _injector: Injector, + @Inject(LocaleService) private readonly _localeService: LocaleService + ) { + super(); + + this._localeService.load({ + zhCN, + enUS, + }); + this._config = Tools.deepMerge({}, DefaultSheetsSortUIConfig, this._config); + } + + override onStarting(injector: Injector): void { + ([ + [SheetsSortUIService], + [SheetsSortUIController, { + useFactory: () => this._injector.createInstance(SheetsSortUIController, this._config), + }], + ] as Dependency[]).forEach((d) => injector.add(d)); + } +} diff --git a/packages/sheets-sort-ui/src/services/sheets-sort-ui.service.tsx b/packages/sheets-sort-ui/src/services/sheets-sort-ui.service.tsx new file mode 100644 index 0000000000..aecbd19d44 --- /dev/null +++ b/packages/sheets-sort-ui/src/services/sheets-sort-ui.service.tsx @@ -0,0 +1,287 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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, Nullable, Workbook, Worksheet } from '@univerjs/core'; +import { Disposable, ICommandService, + ILogService, + IUniverInstanceService, + LifecycleStages, + LocaleService, + LocaleType, + OnLifecycle, + Tools, + UniverInstanceType, +} from '@univerjs/core'; + +import { getPrimaryForRange, NORMAL_SELECTION_PLUGIN_NAME, SelectionManagerService, SetSelectionsOperation } from '@univerjs/sheets'; +import { Inject } from '@wendellhu/redi'; +import type { ISheetRangeLocation } from '@univerjs/sheets-ui'; +import { expandToContinuousRange } from '@univerjs/sheets-ui'; + +import type { ISortOption } from '@univerjs/sheets-sort'; +import { SheetsSortService, SortType } from '@univerjs/sheets-sort'; +import React from 'react'; +import { IConfirmService } from '@univerjs/ui'; +import { BehaviorSubject } from 'rxjs'; +import { ExtendConfirm } from '../views/ExtendConfirm'; + +export enum EXTEND_TYPE { + KEEP = 'keep', + EXTEND = 'extend', + CANCEL = 'cancel', +} + +export interface ICustomSortState { + location?: ISheetSortLocation; + show: boolean; +} + +export interface ISheetSortLocation extends ISheetRangeLocation { + colIndex: number; +} + +const SORT_ERROR_MESSAGE = { + MERGE_ERROR: 'sheets-sort.error.merge-size', + EMPTY_ERROR: 'sheets-sort.error.empty', + SINGLE_ERROR: 'sheets-sort.error.single', + FORMULA_ARRAY: 'sheets-sort.error.formula-array', +}; + +@OnLifecycle(LifecycleStages.Ready, SheetsSortService) +export class SheetsSortUIService extends Disposable { + private readonly _customSortState$ = new BehaviorSubject>(null); + readonly customSortState$ = this._customSortState$.asObservable(); + constructor( + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, + @IConfirmService private readonly _confirmService: IConfirmService, + @ILogService private readonly _logService: ILogService, + @Inject(SelectionManagerService) private readonly _selectionManagerService: SelectionManagerService, + @Inject(SheetsSortService) private readonly _sheetsSortService: SheetsSortService, + @Inject(LocaleService) private readonly _localeService: LocaleService, + @ICommandService private readonly _commandService: ICommandService) { + super(); + } + + async triggerSortDirectly(asc: boolean, extend: boolean, sheetRangeLocation?: ISheetSortLocation): Promise { + const location = sheetRangeLocation || await this._detectSortLocation(extend); + if (!location) { + return false; + } + + const check = this._check(location); + if (!check) { + return false; + } + + const sortOption: ISortOption = { + orderRules: [{ + type: asc ? SortType.ASC : SortType.DESC, + colIndex: location.colIndex, + }], + range: location.range, + }; + this._sheetsSortService.applySort(sortOption, location.unitId, location.subUnitId); + return true; + } + + async triggerSortCustomize() { + const location = await this._detectSortLocation(); + if (!location) { + return false; + } + + const check = this._check(location); + if (!check) { + return false; + } + + // open customize dialog + this.showCustomSortPanel(location); + return true; + } + + customSortState() { + return this._customSortState$.getValue(); + } + + getTitles(hasTitle: boolean) { + const location = this.customSortState()?.location; + if (!location) { + return []; + } + + const { unitId, subUnitId, range } = location; + const worksheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId); + if (!worksheet) { + return []; + } + + const colTranslator = colIndexTranslator(this._localeService); + + return Array.from({ length: range.endColumn - range.startColumn + 1 }, + (_, i) => { + const cellValue = worksheet.getCell(range.startRow, i + range.startColumn)?.v; + return { + index: i + range.startColumn, + label: hasTitle ? + `${cellValue ?? colTranslator(i + range.startColumn)}` : + colTranslator(i + range.startColumn), + }; + }); + } + + setSelection(unitId: string, subUnitId: string, range: IRange) { + const worksheet = (this._univerInstanceService.getUnit(unitId) as Workbook)?.getSheetBySheetId(subUnitId); + if (!worksheet) { + return; + } + const setSelectionsOperationParams = { + unitId, + subUnitId, + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + selections: [{ range, primary: getPrimaryForRange(range, worksheet), style: null }], + + }; + this._commandService.executeCommand(SetSelectionsOperation.id, setSelectionsOperationParams); + } + + async showCheckError(content: string): Promise { + return await this._confirmService.confirm({ + id: 'sort-range-check-error', + title: { + title: this._localeService.t('info.tooltip'), + }, + children: { + title:
{this._localeService.t(content)}
, + }, + }); + } + + async showExtendConfirm(): Promise { + let shouldExtend = false; + const confirm = await this._confirmService.confirm({ + id: 'extend-sort-range-dialog', + title: { + title: this._localeService.t('sheets-sort.dialog.sort-reminder'), + }, + children: { + title: ( + { + shouldExtend = value === '1'; + }} + /> + ), + }, + width: 400, + + }); + if (confirm) { + return shouldExtend ? EXTEND_TYPE.EXTEND : EXTEND_TYPE.KEEP; + } + return EXTEND_TYPE.CANCEL; + } + + showCustomSortPanel(location: ISheetSortLocation) { + this._customSortState$.next({ location, show: true }); + } + + closeCustomSortPanel() { + this._customSortState$.next({ show: false }); + } + + private _check(location: ISheetSortLocation) { + const singleCheck = this._sheetsSortService.singleCheck(location); + if (!singleCheck) { + this.showCheckError(SORT_ERROR_MESSAGE.SINGLE_ERROR); + return false; + } + + const mergeCheck = this._sheetsSortService.mergeCheck(location); + if (!mergeCheck) { + this.showCheckError(SORT_ERROR_MESSAGE.MERGE_ERROR); + return false; + } + + const formulaCheck = this._sheetsSortService.formulaCheck(location); + if (!formulaCheck) { + this.showCheckError(SORT_ERROR_MESSAGE.FORMULA_ARRAY); + return false; + } + + const emptyCheck = this._sheetsSortService.emptyCheck(location); + if (!emptyCheck) { + this.showCheckError(SORT_ERROR_MESSAGE.EMPTY_ERROR); + return false; + } + return true; + } + + private async _detectSortLocation(extend?: boolean): Promise> { + const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET) as Workbook; + const worksheet = workbook.getActiveSheet() as Worksheet; + const unitId = workbook.getUnitId(); + const subUnitId = worksheet.getSheetId(); + const selection = this._selectionManagerService.getLast(); + if (!selection) { + return null; + } + let range: IRange; + if (extend === true) { + range = expandToContinuousRange(selection.range, { up: true, down: true, left: true, right: true }, worksheet); + this.setSelection(unitId, subUnitId, range); + } else if (extend === false) { + range = selection.range; + } else { + const confirmRes = await this.showExtendConfirm(); + if (confirmRes === EXTEND_TYPE.CANCEL) { + return null; + } + if (confirmRes === EXTEND_TYPE.KEEP) { + range = selection.range; + } else { + range = expandToContinuousRange(selection.range, { up: true, down: true, left: true, right: true }, worksheet); + this.setSelection(unitId, subUnitId, range); + } + } + const primary = this._selectionManagerService.getLast()?.primary; + if (!primary) { + return null; + } + + return { + range, + unitId, + subUnitId, + colIndex: primary.actualColumn, + }; + } +} + +function colIndexTranslator(localeService: LocaleService) { + return (colIndex: number) => { + const colName = Tools.chatAtABC(colIndex); + const currentLocale = localeService.getCurrentLocale(); + switch (currentLocale) { + case LocaleType.ZH_CN: + return `"${colName}"列`; + case LocaleType.EN_US: + return `Column "${colName}"`; + default: + return `Column "${colName}"`; + } + }; +} diff --git a/packages/sheets-sort-ui/src/views/CustomSortPanel.tsx b/packages/sheets-sort-ui/src/views/CustomSortPanel.tsx new file mode 100644 index 0000000000..f9ca49cece --- /dev/null +++ b/packages/sheets-sort-ui/src/views/CustomSortPanel.tsx @@ -0,0 +1,265 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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, Nullable } from '@univerjs/core'; +import { LocaleService, LocaleType, throttle } from '@univerjs/core'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { type IOrderRule, SheetsSortService, SortType } from '@univerjs/sheets-sort'; +import { Button, Checkbox, DraggableList, Dropdown, Radio, RadioGroup } from '@univerjs/design'; +import { useDependency } from '@wendellhu/redi/react-bindings'; +import { CheckMarkSingle, DeleteEmptySingle, IncreaseSingle, MoreDownSingle, SequenceSingle } from '@univerjs/icons'; +import { SheetsSortUIService } from '../services/sheets-sort-ui.service'; + +import styles from './index.module.less'; + +export interface ICustomSortPanelProps { + range: IRange; + onListChange: (value: IOrderRule[]) => void; +} + +export function CustomSortPanel() { + const sheetsSortUIService = useDependency(SheetsSortUIService); + const sheetsSortService = useDependency(SheetsSortService); + const localeService = useDependency(LocaleService); + + const [hasTitle, setHasTitle] = useState(false); + const [scrollPosition, setScrollPosition] = useState(0); + const listEndRef = useRef(null); + + const state = sheetsSortUIService.customSortState(); + if (!state || !state.location) { + return null; + } + const { range, unitId, subUnitId } = state.location; + + const titles = sheetsSortUIService.getTitles(hasTitle); + + const [list, setList] = useState([ + { type: SortType.ASC, colIndex: range.startColumn }, + ]); + + const onItemChange = useCallback((index: number, value: Nullable) => { + const newList = [...list]; + if (value === null) { + newList.splice(index, 1); + } else { + newList[index] = value as IOrderRule; + } + + setList(newList as IOrderRule[]); + }, [list]); + + const newItem = useCallback( + throttle(() => { + const newList = [...list]; + const nextColIndex = findNextColIndex(range, list); + if (nextColIndex !== null) { + newList.push({ type: SortType.ASC, colIndex: nextColIndex }); + setList(newList); + } + }, 200), [list, range]); + + const apply = useCallback((orderRules: IOrderRule[], hasTitle: boolean) => { + sheetsSortService.applySort({ range, orderRules, hasTitle }); + sheetsSortUIService.closeCustomSortPanel(); + }, [sheetsSortService, sheetsSortUIService, range]); + + const cancel = useCallback(() => { + sheetsSortUIService.closeCustomSortPanel(); + }, [sheetsSortUIService]); + + const setTitle = useCallback((value: boolean) => { + setHasTitle(value); + if (value) { + sheetsSortUIService.setSelection(unitId, subUnitId, { ...range, startRow: range.startRow + 1 }); + } else { + sheetsSortUIService.setSelection(unitId, subUnitId, range); + } + }, [sheetsSortUIService, range, subUnitId, unitId]); + + useEffect(() => { + if (listEndRef.current && list.length > 5) { + listEndRef.current.scrollTop = listEndRef.current.scrollHeight; + } + }, [list]); + + const canNew = list.length < titles.length; + + const dragList = list.map((item) => ({ ...item, id: `${item.colIndex}` })); + + return ( +
+
{ e.stopPropagation(); }}> +
+
+ setTitle(!!value)}> + {localeService.t('sheets-sort.dialog.first-row-check')} + +
+ {canNew + ? ( +
+ + {localeService.t('sheets-sort.dialog.add-condition')} +
+ ) + : ( +
+ + {localeService.t('sheets-sort.dialog.add-condition')} +
+ )} + +
+
{ + const position = e.currentTarget.scrollTop; + setScrollPosition(position); + }} + ref={listEndRef} + > + ( + onItemChange(index, value)} + scrollPosition={scrollPosition} + /> + )} + rowHeight={32} + margin={[0, 12]} + /> +
+
+
+ + +
+
+ ); +} + +interface ISortOptionItemProps { + titles: { index: number; label: string }[]; + list: IOrderRule[]; + item: IOrderRule; + scrollPosition: number; + onChange: (value: Nullable, index: number) => void; +} + +export function SortOptionItem(props: ISortOptionItemProps) { + const { list, item, titles, onChange, scrollPosition } = props; + const localeService = useDependency(LocaleService); + + const availableMenu = titles.filter((title) => (!list.some((item) => item.colIndex === title.index)) || title.index === item.colIndex); + const currentIndex = list.findIndex((listItem) => listItem.colIndex === item.colIndex); + + const handleChangeColIndex = useCallback((menuItem: { index: number; label: string }) => { + onChange({ ...item, colIndex: menuItem.index }, currentIndex); + setVisible(false); + }, [currentIndex, item, onChange]); + + const [visible, setVisible] = useState(false); + + const onVisibleChange = (visible: boolean) => { + setVisible(visible); + }; + + useEffect(() => { + setVisible(false); + }, [scrollPosition]); + + const showDelete = list.length > 1; + const itemLabel = titles.find((title) => title.index === item.colIndex)?.label; + + const radioClass = localeService.getCurrentLocale() === LocaleType.ZH_CN ? styles.customSortPanelItemOrderRadioCn : styles.customSortPanelItemOrderRadio; + return ( +
+
+
+ +
+
+ + {availableMenu.map((menuItem) => ( +
  • handleChangeColIndex(menuItem)} + className={styles.customSortColMenuItem} + > + + {menuItem.label} + + + {menuItem.index === item.colIndex && ( + + )} + +
  • + ))} + + )} + > +
    + {itemLabel} + +
    +
    +
    +
    +
    + { + onChange({ ...item, type: value as SortType }, currentIndex); + }} + > + {localeService.t('sheets-sort.general.sort-asc')} + {localeService.t('sheets-sort.general.sort-desc')} + +
    +
    + { showDelete && onChange(null, currentIndex)} />} +
    +
    + ); +} + +function findNextColIndex(range: IRange, list: Nullable[]): number | null { + const { startColumn, endColumn } = range; + const used = new Set(list.map((item) => item?.colIndex)); + for (let i = startColumn; i <= endColumn; i++) { + if (!used.has(i)) { + return i; + } + } + return null; +} + diff --git a/packages/sheets-sort-ui/src/views/EmbedSortBtn.tsx b/packages/sheets-sort-ui/src/views/EmbedSortBtn.tsx new file mode 100644 index 0000000000..1bccae9e4f --- /dev/null +++ b/packages/sheets-sort-ui/src/views/EmbedSortBtn.tsx @@ -0,0 +1,61 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback } from 'react'; +import { type IRange, IUniverInstanceService, LocaleService } from '@univerjs/core'; +import { useDependency } from '@wendellhu/redi/react-bindings'; +import { getSheetCommandTarget } from '@univerjs/sheets'; +import { AscendingSingle, DescendingSingle } from '@univerjs/icons'; +import { SheetsSortUIService } from '../services/sheets-sort-ui.service'; +import styles from './index.module.less'; + +export interface IEmbedSortBtnProps { + range: IRange; + colIndex: number; + onClose: () => void; +} + +export default function EmbedSortBtn(props: any) { + const { range, colIndex, onClose } = props as IEmbedSortBtnProps; + + const sheetsSortUIService = useDependency(SheetsSortUIService); + const univerInstanceService = useDependency(IUniverInstanceService); + const localeService = useDependency(LocaleService); + + const apply = useCallback((asc: boolean) => { + const { unitId, subUnitId } = getSheetCommandTarget(univerInstanceService) || {}; + if (range && unitId && subUnitId) { + const noTitleRange = { ...range, startRow: range.startRow + 1 }; + sheetsSortUIService.triggerSortDirectly(asc, false, { unitId, subUnitId, range: noTitleRange, colIndex }); + } else { + console.warn(`Cannot find the target to sort. unitId: ${unitId}, subUnitId: ${subUnitId}, range: ${range}, colIndex: ${colIndex}`); + } + onClose(); + }, [range, colIndex, sheetsSortUIService, univerInstanceService, onClose]); + + return ( +
    +
    apply(true)}> + + {localeService.t('sheets-sort.general.sort-asc')} +
    +
    apply(false)}> + + {localeService.t('sheets-sort.general.sort-desc')} +
    +
    + ); +} diff --git a/packages/sheets-sort-ui/src/views/ExtendConfirm.tsx b/packages/sheets-sort-ui/src/views/ExtendConfirm.tsx new file mode 100644 index 0000000000..156aecbce8 --- /dev/null +++ b/packages/sheets-sort-ui/src/views/ExtendConfirm.tsx @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocaleService } from '@univerjs/core'; +import { Radio, RadioGroup } from '@univerjs/design'; +import { useDependency } from '@wendellhu/redi/react-bindings'; +import React, { useState } from 'react'; +import styles from './index.module.less'; + +export interface IExtendConfirmProps { + onChange: (value: string) => void; +}; + +export const ExtendConfirm = (props: IExtendConfirmProps) => { + const [extend, setExtend] = useState('0'); + const localeService = useDependency(LocaleService); + return ( +
    +
    {localeService.t('sheets-sort.dialog.sort-reminder-desc')}
    + { + setExtend(value as string); + props.onChange(value as string); + }} + > + + {localeService.t('sheets-sort.dialog.sort-reminder-no')} + + + {localeService.t('sheets-sort.dialog.sort-reminder-ext')} + + +
    + ); +}; diff --git a/packages/sheets-sort-ui/src/views/index.module.less b/packages/sheets-sort-ui/src/views/index.module.less new file mode 100644 index 0000000000..342ad6aa6b --- /dev/null +++ b/packages/sheets-sort-ui/src/views/index.module.less @@ -0,0 +1,213 @@ +.extend-confirm-content { + font-size: 14px; +} +.extend-confirm-radio-group { + margin-top: 16px; +} + +.custom-sort-panel-container { + .custom-sort-panel-ext { + display: flex; + justify-content: space-between; + align-items: center; + + .add-condition { + color: var(---Hyacinth-Blue-500, #274fee); + font-size: 16px; + display: flex; + align-items: center; + cursor: pointer; + user-select: none; + &-text { + margin-left: 6px; + font-size: 12px; + } + &-disable { + color: var(---Black, #1e222b); + opacity: 0.3; + cursor: not-allowed; + } + } + } + + .custom-sort-panel-footer { + margin-top: 20px; + display: flex; + justify-content: end; + &-btn { + margin-left: 12px; + } + } + + .custom-sort-panel-item { + display: flex; + align-items: center; + border-bottom: 1px solid var(--gray-200); + + &-head { + display: flex; + align-items: center; + } + + &-handler { + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + color: #1e222b; + cursor: pointer; + } + &-remove { + font-size: 14px; + color: #4b4b4b; + cursor: pointer; + width: 14px; + height: 14px; + position: absolute; + right: 0; + } + &-column-input { + display: flex; + width: 236px; + padding: 6px 10px; + justify-content: space-between; + align-items: center; + margin-left: 8px; + border-radius: 6px; + border: 1px solid var(---Grey-200, #e5e5e5); + background: var(---White, #fff); + color: var(---Black, #1e222b); + font-family: 'PingFang SC'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 20px; + overflow: hidden; + &-dropdown { + width: 16px; + height: 16px; + color: #4b4b4b; + } + } + &-column-input-text { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &-order-radio { + display: flex; + padding: 0 10px; + } + &-order-radio-cn { + display: flex; + padding: 0 20px; + } + } + + .condition-list { + max-height: 310px; + overflow-y: auto; + overflow-x: hidden; + } +} + +.embed-sort-btn-container { + display: flex; + margin-bottom: 12px; + .embed-sort-btn { + font-size: 13px; + font-style: normal; + font-weight: 400; + display: flex; + width: 140px; + padding: 6px 0px; + justify-content: center; + align-items: center; + gap: 4px; + border: 1px solid var(---Grey-200, #e5e5e5); + cursor: default; + + &:active { + background: rgba(30, 34, 43, 0.09); + } + &:hover { + background: var(--black-6, rgba(30, 34, 43, 0.06)); + } + } + .embed-sort-btn-asc { + border-radius: 6px 0px 0px 6px; + } + .embed-sort-btn-desc { + border-radius: 0px 6px 6px 0px; + margin-left: -1px; + } + .embed-sort-btn-icon { + font-size: 16px; + color: #1e222b; + margin-right: 4px; + } +} + +.custom-sort-col-menu { + display: grid; + gap: var(--margin-xxs); + align-items: center; + + box-sizing: border-box; + margin-top: 0; + margin-bottom: 0; + padding: var(--padding-sm); + + font-size: var(--font-size-base); + + background: var(---White, #fff); + border: 1px solid var(--black-6, rgba(30, 34, 43, 0.06)); + border-radius: var(--Radius-4, 8px); + box-shadow: 0px 4px 12px 2px rgba(30, 34, 43, 0.1); + + max-height: 310px; + overflow-y: auto; + overflow-x: hidden; + + li { + list-style: none; + } + + &-item { + cursor: pointer; + + position: relative; + + display: flex; + align-items: center; + justify-content: space-between; + + box-sizing: border-box; + height: 28px; + padding: 0px 8px; + + font-size: 13px; + font-weight: 400; + font-style: normal; + line-height: 20px; /* 153.846% */ + color: var(---Black, #1e222b); + text-align: left; + + border-radius: 4px; + border-radius: var(--border-radius-base); + + transition: background 0.2s; + + &:hover { + background: #f5f5f5; + } + + &-desc { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} diff --git a/packages/sheets-sort-ui/src/vite-env.d.ts b/packages/sheets-sort-ui/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/sheets-sort-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/sheets-sort-ui/tsconfig.json b/packages/sheets-sort-ui/tsconfig.json new file mode 100644 index 0000000000..d676ad2a20 --- /dev/null +++ b/packages/sheets-sort-ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/sheets-sort-ui/tsconfig.node.json b/packages/sheets-sort-ui/tsconfig.node.json new file mode 100644 index 0000000000..e53dac8868 --- /dev/null +++ b/packages/sheets-sort-ui/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/sheets-sort-ui/vite.config.ts b/packages/sheets-sort-ui/vite.config.ts new file mode 100644 index 0000000000..925b530b4d --- /dev/null +++ b/packages/sheets-sort-ui/vite.config.ts @@ -0,0 +1,12 @@ +import createViteConfig from '@univerjs/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({}, { + mode, + pkg, + features: { + react: false, + css: true, + dom: true, + }, +}); diff --git a/packages/sheets-sort/README-zh.md b/packages/sheets-sort/README-zh.md new file mode 100644 index 0000000000..d4bef8deee --- /dev/null +++ b/packages/sheets-sort/README-zh.md @@ -0,0 +1,22 @@ +# @univerjs/sheets-sort + +[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-sort)](https://npmjs.org/packages/@univerjs/sheets-sort) +[![license](https://img.shields.io/npm/l/@univerjs/sheets-sort)](https://img.shields.io/npm/l/@univerjs/sheets-sort) + +## 简介 + +`@univerjs/sheets-sort` 是 Univer Sheet 的排序插件。 + +## 使用指南 + +你应该将此插件与 `@univerjs/sheets-sort-ui` 包一起使用。 + +### 安装 + +```shell +# 使用 npm +npm install @univerjs/sheets-sort + +# 使用 pnpm +pnpm add @univerjs/sheets-sort +``` diff --git a/packages/sheets-sort/README.md b/packages/sheets-sort/README.md new file mode 100644 index 0000000000..959271fab1 --- /dev/null +++ b/packages/sheets-sort/README.md @@ -0,0 +1,22 @@ +# @univerjs/sheets-sort + +[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-sort)](https://npmjs.org/packages/@univerjs/sheets-sort) +[![license](https://img.shields.io/npm/l/@univerjs/sheets-sort)](https://img.shields.io/npm/l/@univerjs/sheets-sort) + +## Introduction + +`@univerjs/sheets-sort` is a sort plugin for Univer Sheet. + +## Usage + +You should use this plugin with the `@univerjs/sheets-sort-ui` package. + +### Installation + +```shell +# Using npm +npm install @univerjs/sheets-sort + +# Using pnpm +pnpm add @univerjs/sheets-sort +``` diff --git a/packages/sheets-sort/package.json b/packages/sheets-sort/package.json new file mode 100644 index 0000000000..bfe2fd4458 --- /dev/null +++ b/packages/sheets-sort/package.json @@ -0,0 +1,81 @@ +{ + "name": "@univerjs/sheets-sort", + "version": "0.1.13", + "private": false, + "description": "A library for sorting data in Univer Sheet", + "author": "DreamNum ", + "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": [], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*", + "./locale/*": "./src/locale/*.ts" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/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" + }, + "./lib/*": "./lib/*" + } + }, + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "lint:types": "tsc --noEmit", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/engine-formula": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@univerjs/sheets-ui": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": "^0.15.2", + "rxjs": ">=7.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/shared": "workspace:*", + "@univerjs/sheets": "workspace:*", + "@wendellhu/redi": "^0.15.2", + "rxjs": "^7.8.1", + "typescript": "^5.4.5", + "vite": "^5.2.10", + "vitest": "^1.5.2" + } +} diff --git a/packages/sheets-sort/src/commands/__tests__/create-command-test-bed.ts b/packages/sheets-sort/src/commands/__tests__/create-command-test-bed.ts new file mode 100644 index 0000000000..351aa09332 --- /dev/null +++ b/packages/sheets-sort/src/commands/__tests__/create-command-test-bed.ts @@ -0,0 +1,188 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BooleanNumber, ILogService, IUniverInstanceService, type IWorkbookData, LocaleService, LocaleType, LogLevel, Plugin, Tools, Univer, UniverInstanceType, type Workbook } from '@univerjs/core'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; +import { SheetsRenderService } from '@univerjs/sheets-ui'; +import { SelectionManagerService, SheetInterceptorService } from '@univerjs/sheets'; +import { FormulaDataModel } from '@univerjs/engine-formula'; +import { enUS } from '../../locale'; +import { SheetsSortService } from '../../services/sheets-sort.service'; +import { SheetsSortController } from '../../controllers/sheets-sort.controller'; + +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(); + + this._injector = _injector; + } + + override onStarting(injector: Injector): void { + injector.add([SheetsSortService]); + injector.add([SheetsSortController]); + injector.add([SheetsRenderService, { useClass: mockSheetsRenderService }]); + injector.add([SelectionManagerService, { useClass: mockSelectionManagerService as any }]); + injector.add([SheetInterceptorService]); + injector.add([FormulaDataModel, { useClass: mockFormulaDataModel }]); + + dependencies?.forEach((d) => injector.add(d)); + } + } + + univer.registerPlugin(TestPlugin); + const sheet = univer.createUniverSheet(Tools.deepClone(workbookData || TEST_WORKBOOK_DATA_DEMO)); + + const univerInstanceService = injector.get(IUniverInstanceService); + univerInstanceService.focusUnit('test'); + const logService = injector.get(ILogService); + + logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs + + const localeService = injector.get(LocaleService); + localeService.load({ enUS }); + + return { + univer, + get: injector.get.bind(injector), + sheet, + }; +} + +class mockSheetsRenderService { + registerSkeletonChangingMutations(id: string) { + } +} + +class mockSelectionManagerService { + replace() { + } +} + +class mockFormulaDataModel { + getArrayFormulaRange() { + } +} diff --git a/packages/sheets-sort/src/commands/__tests__/sheets-reorder.command.spec.ts b/packages/sheets-sort/src/commands/__tests__/sheets-reorder.command.spec.ts new file mode 100644 index 0000000000..6efadeff03 --- /dev/null +++ b/packages/sheets-sort/src/commands/__tests__/sheets-reorder.command.spec.ts @@ -0,0 +1,127 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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, Univer, Workbook } from '@univerjs/core'; +import { ICommandService, IUniverInstanceService } from '@univerjs/core'; +import type { Injector } from '@wendellhu/redi'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ReorderRangeCommand, ReorderRangeMutation, SetSelectionsOperation } from '@univerjs/sheets'; +import type { ISortRangeCommandParams } from '../sheets-sort.command'; +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; + + 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); + }); + }); +}); + diff --git a/packages/sheets-sort/src/commands/sheets-sort.command.ts b/packages/sheets-sort/src/commands/sheets-sort.command.ts new file mode 100644 index 0000000000..62c9b464c2 --- /dev/null +++ b/packages/sheets-sort/src/commands/sheets-sort.command.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { CommandType, ICommandService, IUniverInstanceService, Rectangle, sequenceExecute } from '@univerjs/core'; +import type { ICellData, ICommand, IRange, Nullable, Worksheet } from '@univerjs/core'; +import type { IReorderRangeCommandParams, ISheetCommandSharedParams } from '@univerjs/sheets'; +import { getSheetCommandTarget, ReorderRangeCommand } from '@univerjs/sheets'; +import type { IAccessor } from '@wendellhu/redi'; +import type { IOrderRule, SortType } from '../services/interface'; +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>; +} + +export enum ORDER { + KEEP = 1, + EXCHANGE = -1, + EQUAL = 0, +} + +export type CellValue = number | string | null; + +export type ICellValueCompareFn = (type: SortType, a: Nullable, b: Nullable) => Nullable; + +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[] { + const result: Nullable[] = []; + orderRules.forEach(({ colIndex }) => { + result.push(worksheet.getCellRaw(rowIndex, colIndex)); + }); + return result; +} + +function combineCompareFnsAsOne(compareFns: ICellValueCompareFn[]) { + return (type: SortType, a: Nullable, b: Nullable) => { + 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 = 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; + }; +} + diff --git a/packages/sheets-sort/src/controllers/sheets-sort.controller.ts b/packages/sheets-sort/src/controllers/sheets-sort.controller.ts new file mode 100644 index 0000000000..3049232007 --- /dev/null +++ b/packages/sheets-sort/src/controllers/sheets-sort.controller.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICellData, Nullable } from '@univerjs/core'; +import { CellValueType, Disposable, ICommandService, LifecycleStages, OnLifecycle } from '@univerjs/core'; + +import { Inject } from '@wendellhu/redi'; +import { SheetsRenderService } from '@univerjs/sheets-ui'; +import type { ICellValueCompareFn } from '../commands/sheets-sort.command'; +import { SortRangeCommand } from '../commands/sheets-sort.command'; +import { SheetsSortService } from '../services/sheets-sort.service'; +import { compareNull, compareNumber, compareString, isNullValue } from './utils'; + +export type ICommonComparableCellValue = number | string | null; + +@OnLifecycle(LifecycleStages.Ready, SheetsSortController) +export class SheetsSortController extends Disposable { + constructor( + @ICommandService private readonly _commandService: ICommandService, + @Inject(SheetsRenderService) private _sheetRenderService: SheetsRenderService, + @Inject(SheetsSortService) private readonly _sortService: SheetsSortService + ) { + super(); + [ + SortRangeCommand, + ].forEach((m) => this.disposeWithMe(this._sheetRenderService.registerSkeletonChangingMutations(m.id))); + 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): 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}`; + } +} + diff --git a/packages/sheets-sort/src/controllers/utils.ts b/packages/sheets-sort/src/controllers/utils.ts new file mode 100644 index 0000000000..373dfc3186 --- /dev/null +++ b/packages/sheets-sort/src/controllers/utils.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 { SortType } from '../services/interface'; +import type { ICommonComparableCellValue } from './sheets-sort.controller'; + +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) => { + if (!cell) { + return true; + } + if (Object.keys(cell).length === 0) { + return true; + } + if (cell?.v == null && cell?.p == null) { + return true; + } + return false; +}; + diff --git a/packages/sheets-sort/src/index.ts b/packages/sheets-sort/src/index.ts new file mode 100644 index 0000000000..b18c03b546 --- /dev/null +++ b/packages/sheets-sort/src/index.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { UniverSheetsSortPlugin } from './plugin'; + +export { + type ISortRangeCommandParams, + SortRangeCommand, +} from './commands/sheets-sort.command'; + +export { SheetsSortService } from './services/sheets-sort.service'; +export { type ISortOption, SortType, type IOrderRule } from './services/interface'; diff --git a/packages/sheets-sort/src/locale/en-US.ts b/packages/sheets-sort/src/locale/en-US.ts new file mode 100644 index 0000000000..3afc744b90 --- /dev/null +++ b/packages/sheets-sort/src/locale/en-US.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + sheetsSort: { + title: 'Sort', + toolbar: { + 'sort-asc': 'ascending order', + 'sort-desc': 'descending order', + 'sort-custom': 'custom order', + }, + 'context-menu': { + 'sort-asc': 'ascending order', + 'sort-desc': 'descending order', + 'sort-custom': 'custom order', + }, + }, +}; + +export default locale; diff --git a/packages/sheets-sort/src/locale/index.ts b/packages/sheets-sort/src/locale/index.ts new file mode 100644 index 0000000000..1eff5fc204 --- /dev/null +++ b/packages/sheets-sort/src/locale/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { default as enUS } from './en-US'; +export { default as zhCN } from './zh-CN'; diff --git a/packages/sheets-sort/src/locale/ru-RU.ts b/packages/sheets-sort/src/locale/ru-RU.ts new file mode 100644 index 0000000000..3afc744b90 --- /dev/null +++ b/packages/sheets-sort/src/locale/ru-RU.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type zhCN from './zh-CN'; + +const locale: typeof zhCN = { + sheetsSort: { + title: 'Sort', + toolbar: { + 'sort-asc': 'ascending order', + 'sort-desc': 'descending order', + 'sort-custom': 'custom order', + }, + 'context-menu': { + 'sort-asc': 'ascending order', + 'sort-desc': 'descending order', + 'sort-custom': 'custom order', + }, + }, +}; + +export default locale; diff --git a/packages/sheets-sort/src/locale/zh-CN.ts b/packages/sheets-sort/src/locale/zh-CN.ts new file mode 100644 index 0000000000..76d53c36d1 --- /dev/null +++ b/packages/sheets-sort/src/locale/zh-CN.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = { + sheetsSort: { + title: '排序', + toolbar: { + 'sort-asc': '升序', + 'sort-desc': '降序', + 'sort-custom': '自定义排序', + }, + 'context-menu': { + 'sort-asc': '升序', + 'sort-desc': '降序', + 'sort-custom': '自定义排序', + }, + }, +}; + +export default locale; diff --git a/packages/sheets-sort/src/plugin.ts b/packages/sheets-sort/src/plugin.ts new file mode 100644 index 0000000000..ef59b8d10b --- /dev/null +++ b/packages/sheets-sort/src/plugin.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocaleService, Plugin, UniverInstanceType } from '@univerjs/core'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; + +import { SheetsSortController } from './controllers/sheets-sort.controller'; +import { SheetsSortService } from './services/sheets-sort.service'; +import { enUS, zhCN } from './locale'; + +const NAME = 'UNIVER_SHEETS_SORT_PLUGIN'; + +export class UniverSheetsSortPlugin extends Plugin { + static override type = UniverInstanceType.UNIVER_SHEET; + static override pluginName = NAME; + + constructor( + _config: unknown, + @Inject(Injector) protected readonly _injector: Injector, + @Inject(LocaleService) private readonly _localeService: LocaleService + ) { + super(); + } + + override onStarting(injector: Injector): void { + ([ + [SheetsSortController], + [SheetsSortService], + ] as Dependency[]).forEach((d) => injector.add(d)); + + this._localeService.load({ + zhCN, + enUS, + }); + } +} diff --git a/packages/sheets-sort/src/services/interface.ts b/packages/sheets-sort/src/services/interface.ts new file mode 100644 index 0000000000..fc0454a34a --- /dev/null +++ b/packages/sheets-sort/src/services/interface.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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; +} diff --git a/packages/sheets-sort/src/services/sheets-sort.service.ts b/packages/sheets-sort/src/services/sheets-sort.service.ts new file mode 100644 index 0000000000..5ed9a1af16 --- /dev/null +++ b/packages/sheets-sort/src/services/sheets-sort.service.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 { Disposable, ICommandService, + IUniverInstanceService, + LifecycleStages, + OnLifecycle, + Rectangle, +} from '@univerjs/core'; +import type { ISheetRangeLocation } from '@univerjs/sheets-ui'; + +import { getSheetCommandTarget } from '@univerjs/sheets'; +import { Inject } from '@wendellhu/redi'; +import { FormulaDataModel } from '@univerjs/engine-formula'; +import { type ICellValueCompareFn, SortRangeCommand } from '../commands/sheets-sort.command'; +import { isNullValue } from '../controllers/utils'; +import type { ISortOption } from './interface'; + +@OnLifecycle(LifecycleStages.Ready, SheetsSortService) +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; +} diff --git a/packages/sheets-sort/src/vite-env.d.ts b/packages/sheets-sort/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/sheets-sort/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/sheets-sort/tsconfig.json b/packages/sheets-sort/tsconfig.json new file mode 100644 index 0000000000..d676ad2a20 --- /dev/null +++ b/packages/sheets-sort/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/sheets-sort/tsconfig.node.json b/packages/sheets-sort/tsconfig.node.json new file mode 100644 index 0000000000..e53dac8868 --- /dev/null +++ b/packages/sheets-sort/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/sheets-sort/vite.config.ts b/packages/sheets-sort/vite.config.ts new file mode 100644 index 0000000000..67b2fff8f2 --- /dev/null +++ b/packages/sheets-sort/vite.config.ts @@ -0,0 +1,7 @@ +import createViteConfig from '@univerjs/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({}, { + mode, + pkg, +}); diff --git a/packages/sheets-ui/src/basics/const/index.ts b/packages/sheets-ui/src/basics/const/index.ts index 1346a59a4d..82f7b0bf5f 100644 --- a/packages/sheets-ui/src/basics/const/index.ts +++ b/packages/sheets-ui/src/basics/const/index.ts @@ -15,3 +15,4 @@ */ export * from './plugin-name'; +export * from './ui-name.ts'; diff --git a/packages/sheets-ui/src/basics/const/ui-name.ts b/packages/sheets-ui/src/basics/const/ui-name.ts new file mode 100644 index 0000000000..1ce8451c9e --- /dev/null +++ b/packages/sheets-ui/src/basics/const/ui-name.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 SheetsUIPart { + FILTER_PANEL_EMBED_POINT = 'filter-panel-embed-point', +} diff --git a/packages/sheets-ui/src/commands/commands/__tests__/auto-fill.controller.spec.ts b/packages/sheets-ui/src/commands/commands/__tests__/auto-fill.controller.spec.ts index 604766fa1a..a4f99489c6 100644 --- a/packages/sheets-ui/src/commands/commands/__tests__/auto-fill.controller.spec.ts +++ b/packages/sheets-ui/src/commands/commands/__tests__/auto-fill.controller.spec.ts @@ -46,12 +46,18 @@ import { ISelectionRenderService, SelectionRenderService } from '../../../servic import { SheetSkeletonManagerService } from '../../../services/sheet-skeleton-manager.service'; import { RefillCommand } from '../refill.command'; import { AutoClearContentCommand, AutoFillCommand } from '../auto-fill.command'; +import { SheetsRenderService } from '../../../services/sheets-render.service'; import { createCommandTestBed } from './create-command-test-bed'; const theme = { colorBlack: '#35322b', }; +class mockSheetsRenderService { + registerSkeletonChangingMutations(id: string) { + } +} + const TEST_WORKBOOK_DATA = { id: 'test', appVersion: '3.0.0-alpha', @@ -285,6 +291,7 @@ describe('Test auto fill rules in controller', () => { [IRenderManagerService, { useClass: RenderManagerService }], [SheetSkeletonManagerService], [AutoFillController], + [SheetsRenderService, { useClass: mockSheetsRenderService }], ]); univer = testBed.univer; get = testBed.get; @@ -648,9 +655,9 @@ describe('Test auto fill rules in controller', () => { endRow: 10, endColumn: 1, }); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(3); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(4); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(5); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(2); // undo redo await commandService.executeCommand(UndoCommand.id); @@ -659,9 +666,9 @@ describe('Test auto fill rules in controller', () => { expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(undefined); await commandService.executeCommand(RedoCommand.id); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(3); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(4); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(5); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(2); }); }); @@ -831,8 +838,8 @@ describe('Test auto fill rules in controller', () => { endColumn: 4, } ); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 3)?.v).toBe(3); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 4)?.v).toBe(4); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 3)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 4)?.v).toBe(2); // test left (autoFillController as any)._triggerAutoFill( { @@ -848,8 +855,8 @@ describe('Test auto fill rules in controller', () => { endColumn: 2, } ); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 1)?.v).toBe(1); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 0)?.v).toBe(0); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 1)?.v).toBe(2); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 0)?.v).toBe(2); // test up (autoFillController as any)._triggerAutoFill( { @@ -865,7 +872,7 @@ describe('Test auto fill rules in controller', () => { endColumn: 2, } ); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(15, 2)?.v).toBe(1); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(15, 2)?.v).toBe(2); // test down (autoFillController as any)._triggerAutoFill( { @@ -881,7 +888,7 @@ describe('Test auto fill rules in controller', () => { endColumn: 2, } ); - expect(workbook.getSheetBySheetId('sheet1')?.getCell(17, 2)?.v).toBe(3); + expect(workbook.getSheetBySheetId('sheet1')?.getCell(17, 2)?.v).toBe(2); }); }); }); diff --git a/packages/sheets-ui/src/controllers/auto-fill.controller.ts b/packages/sheets-ui/src/controllers/auto-fill.controller.ts index e76bfc6ab7..9747e93734 100644 --- a/packages/sheets-ui/src/controllers/auto-fill.controller.ts +++ b/packages/sheets-ui/src/controllers/auto-fill.controller.ts @@ -59,7 +59,6 @@ import { SetWorksheetActiveOperation, SetWorksheetColWidthMutation, SetWorksheetRowHeightMutation, - SheetInterceptorService, } from '@univerjs/sheets'; import { Inject, Injector } from '@wendellhu/redi'; @@ -80,6 +79,7 @@ import { IEditorBridgeService } from '../services/editor-bridge.service'; import { ISelectionRenderService } from '../services/selection/selection-render.service'; import { SetCellEditVisibleOperation } from '../commands/operations/cell-edit.operation'; import { SetZoomRatioOperation } from '../commands/operations/set-zoom-ratio.operation'; +import { SheetsRenderService } from '../services/sheets-render.service'; import type { IDiscreteRange } from './utils/range-tools'; import { discreteRangeToRange, generateNullCellValueRowCol, rangeToDiscreteRange } from './utils/range-tools'; @@ -96,9 +96,9 @@ export class AutoFillController extends Disposable { @ICommandService private readonly _commandService: ICommandService, @IAutoFillService private readonly _autoFillService: IAutoFillService, @IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService, - @Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService, @Inject(SelectionManagerService) private readonly _selectionManagerService: SelectionManagerService, - @Inject(Injector) private readonly _injector: Injector + @Inject(Injector) private readonly _injector: Injector, + @Inject(SheetsRenderService) private _sheetsRenderService: SheetsRenderService ) { super(); this._defaultHook = { @@ -120,6 +120,11 @@ export class AutoFillController extends Disposable { this._onSelectionControlFillChanged(); this._onApplyTypeChanged(); this._initQuitListener(); + this._initSkeletonChange(); + } + + private _initSkeletonChange() { + this.disposeWithMe(this._sheetsRenderService.registerSkeletonChangingMutations(AutoFillCommand.id)); } private _initDefaultHook() { diff --git a/packages/sheets-ui/src/services/auto-fill/tools.ts b/packages/sheets-ui/src/services/auto-fill/tools.ts index 434a314569..ba529ae878 100644 --- a/packages/sheets-ui/src/services/auto-fill/tools.ts +++ b/packages/sheets-ui/src/services/auto-fill/tools.ts @@ -444,7 +444,7 @@ export function forecast(x: number, yArr: number[], xArr: number[], forward = tr let b; if (sum_n === 0) { - b = forward ? 1 : -1; + b = 0; } else { b = sum_d / sum_n; } diff --git a/packages/sheets/src/basics/const/command-listener-const.ts b/packages/sheets/src/basics/const/command-listener-const.ts index c26c39a723..dff7df295f 100644 --- a/packages/sheets/src/basics/const/command-listener-const.ts +++ b/packages/sheets/src/basics/const/command-listener-const.ts @@ -20,6 +20,7 @@ import { MoveRangeMutation } from '../../commands/mutations/move-range.mutation' import { MoveColsMutation, MoveRowsMutation } from '../../commands/mutations/move-rows-cols.mutation'; import { RemoveColMutation, RemoveRowMutation } from '../../commands/mutations/remove-row-col.mutation'; import { RemoveWorksheetMergeMutation } from '../../commands/mutations/remove-worksheet-merge.mutation'; +import { ReorderRangeMutation } from '../../commands/mutations/reorder-range.mutation'; import { SetColHiddenMutation, SetColVisibleMutation } from '../../commands/mutations/set-col-visible.mutation'; import { SetRangeValuesMutation } from '../../commands/mutations/set-range-values.mutation'; import { SetRowHiddenMutation, SetRowVisibleMutation } from '../../commands/mutations/set-row-visible.mutation'; @@ -57,4 +58,5 @@ export const COMMAND_LISTENER_VALUE_CHANGE = [ MoveRangeMutation.id, RemoveWorksheetMergeMutation.id, AddWorksheetMergeMutation.id, + ReorderRangeMutation.id, ]; diff --git a/packages/sheets/src/basics/utils.ts b/packages/sheets/src/basics/utils.ts index b9693b7d95..19714f9f38 100644 --- a/packages/sheets/src/basics/utils.ts +++ b/packages/sheets/src/basics/utils.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import type { ICellData, IObjectMatrixPrimitiveType, IRange } from '@univerjs/core'; +import type { ICellData, IObjectMatrixPrimitiveType, IRange, Worksheet } from '@univerjs/core'; import { ObjectMatrix } from '@univerjs/core'; import type { Nullable } from 'vitest'; +import type { IExpandParams } from '../commands/commands/utils/selection-utils'; export const groupByKey = >(arr: T[], key: string, blankKey = '') => { return arr.reduce( @@ -52,6 +53,111 @@ export const createUniqueKey = (initValue = 0) => { }; }; +function cellHasValue(cell: ICellData): boolean { + return (cell.v !== undefined && cell.v !== null && cell.v !== '') || cell.p !== undefined; +} + +// eslint-disable-next-line max-lines-per-function +export function expandToContinuousRange(startRange: IRange, directions: IExpandParams, worksheet: Worksheet): IRange { + const { left, right, up, down } = directions; + const maxRow = worksheet.getMaxRows(); + const maxColumn = worksheet.getMaxColumns(); + + let changed = true; + const destRange: IRange = { ...startRange }; // startRange should not be used below + + while (changed) { + changed = false; + + if (up && destRange.startRow !== 0) { + // see if there are value in the upper row of contents + // set `changed` to true if `startRow` really changes + const destRow = destRange.startRow - 1; // it may decrease if there are merged cell + const matrixFromLastRow = worksheet.getMatrixWithMergedCells( + destRow, + destRange.startColumn, + destRow, + destRange.endColumn + ); + + // we should check if there are value in the upper row of contents, if it does + // we should update the `destRange` and set `changed` to true + matrixFromLastRow.forValue((row, col, value) => { + if (cellHasValue(value)) { + destRange.startRow = Math.min(row, destRange.startRow); + destRange.startColumn = Math.min(col, destRange.startColumn); + destRange.endColumn = Math.max(col, destRange.endColumn); + changed = true; + } + }); + } + + if (down && destRange.endRow !== maxRow - 1) { + const destRow = destRange.endRow + 1; + const matrixFromLastRow = worksheet.getMatrixWithMergedCells( + destRow, + destRange.startColumn, + destRow, + destRange.endColumn + ); + + matrixFromLastRow.forValue((row, col, value) => { + if (cellHasValue(value)) { + destRange.endRow = Math.max( + row + (value.rowSpan !== undefined ? value.rowSpan - 1 : 0), + destRange.endRow + ); + destRange.startColumn = Math.min(col, destRange.startColumn); + destRange.endColumn = Math.max(col, destRange.endColumn); + changed = true; + } + }); + } + + if (left && destRange.startColumn !== 0) { + const destCol = destRange.startColumn - 1; + const matrixFromLastCol = worksheet.getMatrixWithMergedCells( + destRange.startRow, + destCol, + destRange.endRow, + destCol + ); + + matrixFromLastCol.forValue((row, col, value) => { + if (cellHasValue(value)) { + destRange.startColumn = Math.min(col, destRange.startColumn); + destRange.startRow = Math.min(row, destRange.startRow); + destRange.endRow = Math.max(row, destRange.endRow); + changed = true; + } + }); + } + + if (right && destRange.endColumn !== maxColumn - 1) { + const destCol = destRange.endColumn + 1; + const matrixFromLastCol = worksheet.getMatrixWithMergedCells( + destRange.startRow, + destCol, + destRange.endRow, + destCol + ); + + matrixFromLastCol.forValue((row, col, value) => { + if (cellHasValue(value)) { + destRange.endColumn = Math.max( + col + (value.colSpan !== undefined ? value.colSpan - 1 : 0), + destRange.endColumn + ); + destRange.startRow = Math.min(row, destRange.startRow); + destRange.endRow = Math.max(row, destRange.endRow); + changed = true; + } + }); + } + } + + return destRange; +} /** * Generate cellValue from range and set null * @param range diff --git a/packages/sheets/src/commands/commands/reorder-range.command.ts b/packages/sheets/src/commands/commands/reorder-range.command.ts new file mode 100644 index 0000000000..a43c82499c --- /dev/null +++ b/packages/sheets/src/commands/commands/reorder-range.command.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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, IRange } from '@univerjs/core'; +import { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core'; +import type { IAccessor } from '@wendellhu/redi'; +import type { ISheetCommandSharedParams } from '../utils/interface'; +import type { IReorderRangeMutationParams } from '../mutations/reorder-range.mutation'; +import { ReorderRangeMutation, ReorderRangeUndoMutationFactory } from '../mutations/reorder-range.mutation'; +import { SheetInterceptorService } from '../../services/sheet-interceptor/sheet-interceptor.service'; + +export interface IReorderRangeCommandParams extends ISheetCommandSharedParams { + range: IRange; + order: { [key: number]: number }; +} + +export const ReorderRangeCommandId = 'sheet.command.reorder-range' as const; + +export const ReorderRangeCommand: ICommand = { + id: ReorderRangeCommandId, + type: CommandType.COMMAND, + handler: (accessor: IAccessor, params: IReorderRangeCommandParams) => { + const { subUnitId, unitId, range, order } = params; + const commandService = accessor.get(ICommandService); + + const reorderMutation = { + id: ReorderRangeMutation.id, + params: { + unitId, + subUnitId, + order, + range, + } as IReorderRangeMutationParams, + }; + + const undoReorderMutation = { + id: ReorderRangeMutation.id, + params: ReorderRangeUndoMutationFactory(reorderMutation.params), + }; + const sheetInterceptorService = accessor.get(SheetInterceptorService); + const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: ReorderRangeCommand.id, params }); + + const redos = [ + ...(interceptorCommands.preRedos ?? []), + reorderMutation, + ...interceptorCommands.redos, + ]; + + const undos = [ + ...(interceptorCommands.preUndos ?? []), + undoReorderMutation, + ...interceptorCommands.undos, + ]; + const result = sequenceExecute(redos, commandService); + if (result.result) { + const undoRedoService = accessor.get(IUndoRedoService); + undoRedoService.pushUndoRedo({ + unitID: unitId, + undoMutations: undos, + redoMutations: redos, + }); + return true; + } + return false; + }, + +}; diff --git a/packages/sheets/src/commands/mutations/reorder-range.mutation.ts b/packages/sheets/src/commands/mutations/reorder-range.mutation.ts new file mode 100644 index 0000000000..c1305334a9 --- /dev/null +++ b/packages/sheets/src/commands/mutations/reorder-range.mutation.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommandType, IUniverInstanceService, ObjectMatrix, Range, Tools } from '@univerjs/core'; +import type { ICellData, IMutation, IRange, Nullable, Workbook } from '@univerjs/core'; +import type { ISheetCommandSharedParams } from '@univerjs/sheets'; + +export interface IReorderRangeMutationParams extends ISheetCommandSharedParams { + range: IRange; + order: { [key: number]: number }; +} + +export const ReorderRangeUndoMutationFactory = (params: IReorderRangeMutationParams): IReorderRangeMutationParams => { + const { order } = params; + const newOrder = {} as { [key: number]: number }; + Object.keys(order).forEach((key) => { + newOrder[order[Number(key)]] = Number(key); + }); + return { + ...params, + order: newOrder, + }; +}; + +export const ReorderRangeMutation: IMutation = { + id: 'sheet.mutation.reorder-range', + type: CommandType.MUTATION, + handler: (accessor, params) => { + const { subUnitId, unitId, range, order } = params; + const univerInstanceService = accessor.get(IUniverInstanceService); + const workbook = univerInstanceService.getUnit(unitId) as Workbook; + const worksheet = workbook.getSheetBySheetId(subUnitId); + if (!worksheet) { + return false; + } + const cellDataMatrix = new ObjectMatrix>(); + Range.foreach(range, (row, col) => { + if (order.hasOwnProperty(row)) { + const targetRow = order[row]; + const cloneCell = Tools.deepClone(worksheet.getCellRaw(targetRow, col)); + cellDataMatrix.setValue(row, col, cloneCell); + } + }); + + const worksheetCellDataMatrix = worksheet.getCellMatrix(); + + cellDataMatrix.forValue((row, col, cellData) => { + worksheetCellDataMatrix.setValue(row, col, cellData); + }); + return true; + }, + +}; diff --git a/packages/sheets/src/controllers/basic-worksheet.controller.ts b/packages/sheets/src/controllers/basic-worksheet.controller.ts index 1f7d989171..a08d305ef9 100644 --- a/packages/sheets/src/controllers/basic-worksheet.controller.ts +++ b/packages/sheets/src/controllers/basic-worksheet.controller.ts @@ -129,6 +129,8 @@ import { AddRangeProtectionMutation } from '../commands/mutations/add-range-prot import { DeleteRangeProtectionMutation } from '../commands/mutations/delete-range-protection.mutation'; import { SetRangeProtectionMutation } from '../commands/mutations/set-range-protection.mutation'; import { AddWorksheetMergeMutation } from '../commands/mutations/add-worksheet-merge.mutation'; +import { ReorderRangeCommand } from '../commands/commands/reorder-range.command'; +import { ReorderRangeMutation } from '../commands/mutations/reorder-range.mutation'; import { MAX_CELL_PER_SHEET_DEFAULT, MAX_CELL_PER_SHEET_KEY } from './config/config'; import { ONLY_REGISTER_FORMULA_RELATED_MUTATIONS_KEY } from './config'; @@ -167,6 +169,7 @@ export class BasicWorksheetController extends Disposable implements IDisposable SetWorkbookNameMutation, SetWorksheetNameMutation, SetNumfmtMutation, + ReorderRangeMutation, EmptyMutation, ] as IMutation[]).forEach((mutation) => { this._commandService.registerCommand(mutation); @@ -199,6 +202,7 @@ export class BasicWorksheetController extends Disposable implements IDisposable RemoveColCommand, RemoveRowCommand, RemoveSheetCommand, + ReorderRangeCommand, RemoveWorksheetMergeCommand, ResetBackgroundColorCommand, diff --git a/packages/sheets/src/index.ts b/packages/sheets/src/index.ts index 48f7678d08..da004a3d52 100644 --- a/packages/sheets/src/index.ts +++ b/packages/sheets/src/index.ts @@ -171,6 +171,8 @@ export { SetWorksheetRowIsAutoHeightCommand, } from './commands/commands/set-worksheet-row-height.command'; export { SetWorksheetShowCommand } from './commands/commands/set-worksheet-show.command'; +export { ReorderRangeCommand, type IReorderRangeCommandParams } from './commands/commands/reorder-range.command'; +export { ReorderRangeMutation, type IReorderRangeMutationParams } from './commands/mutations/reorder-range.mutation'; export { followSelectionOperation, getPrimaryForRange } from './commands/commands/utils/selection-utils'; export { AddMergeUndoMutationFactory, @@ -305,6 +307,7 @@ export { SetDefinedNameCommand, type ISetDefinedNameCommandParams } from './comm export { ScrollToCellOperation } from './commands/operations/scroll-to-cell.operation'; export type { FormatType } from './services/numfmt/type'; export { getMoveRangeUndoRedoMutations } from './commands/commands/move-range.command'; +export { expandToContinuousRange } from './basics/utils'; // permission export { defaultWorksheetPermissionPoint, getAllWorksheetPermissionPoint, getAllWorksheetPermissionPointByPointPanel } from './services/permission'; diff --git a/packages/sheets/src/services/ref-range/ref-range.service.ts b/packages/sheets/src/services/ref-range/ref-range.service.ts index 290fbae91b..2989c9ec1d 100644 --- a/packages/sheets/src/services/ref-range/ref-range.service.ts +++ b/packages/sheets/src/services/ref-range/ref-range.service.ts @@ -267,6 +267,22 @@ export class RefRangeService extends Disposable { }; return this._checkRange([effectRange], unitId, subUnitId); } + case EffectRefRangId.ReorderRangeCommandId: { + const params = command; + const { range, order } = params.params!; + const effectRanges = []; + for (let row = range.startRow; row <= range.endRow; row++) { + if (row in order) { + effectRanges.push({ + startRow: row, + endRow: row, + startColumn: range.startColumn, + endColumn: range.endColumn, + }); + } + } + return this._checkRange(effectRanges, unitId, subUnitId); + } } }; const cbList = getEffectsCbList() || []; diff --git a/packages/sheets/src/services/ref-range/type.ts b/packages/sheets/src/services/ref-range/type.ts index e4a9d46d64..7ffec87781 100644 --- a/packages/sheets/src/services/ref-range/type.ts +++ b/packages/sheets/src/services/ref-range/type.ts @@ -32,6 +32,8 @@ import type { IMoveColsCommandParams, IMoveRowsCommandParams } from '../../comma import { MoveColsCommandId, MoveRowsCommandId } from '../../commands/commands/move-rows-cols.command'; import type { IRemoveRowColCommandParams } from '../../commands/commands/remove-row-col.command'; import { RemoveColCommandId, RemoveRowCommandId } from '../../commands/commands/remove-row-col.command'; +import type { IReorderRangeCommandParams } from '../../commands/commands/reorder-range.command'; +import { ReorderRangeCommandId } from '../../commands/commands/reorder-range.command'; export type IMoveRowsCommand = ICommandInfo & { id: typeof MoveRowsCommandId }; export type IMoveColsCommand = ICommandInfo & { id: typeof MoveColsCommandId }; @@ -55,6 +57,8 @@ export type IInsertRangeMoveRightCommand = ICommandInfo & { id: typeof ReorderRangeCommandId }; + export type EffectRefRangeParams = | IMoveRangeCommand | IInsertRowCommand @@ -65,7 +69,8 @@ export type EffectRefRangeParams = | IInsertRangeMoveDownCommand | IInsertRangeMoveRightCommand | IMoveColsCommand - | IMoveRowsCommand; + | IMoveRowsCommand + | IReorderRangeCommand; export const EffectRefRangId = { MoveRangeCommandId, @@ -79,6 +84,7 @@ export const EffectRefRangId = { InsertRangeMoveRightCommandId, MoveColsCommandId, MoveRowsCommandId, + ReorderRangeCommandId, } as const; export enum OperatorType { diff --git a/packages/sheets/src/services/ref-range/util.ts b/packages/sheets/src/services/ref-range/util.ts index 6440826837..a2ed8f27be 100644 --- a/packages/sheets/src/services/ref-range/util.ts +++ b/packages/sheets/src/services/ref-range/util.ts @@ -38,6 +38,7 @@ import type { IMoveRowsCommand, IOperator, IRemoveRowColCommand, + IReorderRangeCommand, } from './type'; import { EffectRefRangId, OperatorType } from './type'; @@ -274,6 +275,32 @@ export const handleMoveRowsCommon = (params: IMoveRowsCommand, targetRange: IRan return res; }; +export const handleReorderRangeCommon = (param: IReorderRangeCommand, targetRange: IRange) => { + const { range, order } = param.params || {}; + if (!range || !order) { + return [targetRange]; + } + const matrix = new ObjectMatrix(); + Range.foreach(targetRange, (row, col) => { + matrix.setValue(row, col, 1); + }); + + const cacheMatrix = new ObjectMatrix(); + Range.foreach(range, (row, col) => { + if (order.hasOwnProperty(row)) { + const targetRow = order[row]; + const cloneCell = matrix.getValue(targetRow, col) ?? 0; + cacheMatrix.setValue(row, col, cloneCell); + } + }); + + cacheMatrix.forValue((row, col, cellData) => { + matrix.setValue(row, col, cellData); + }); + const res = queryObjectMatrix(matrix, (value) => value === 1); + return res; +}; + export const handleMoveCols = (params: IMoveColsCommand, targetRange: IRange): IOperator[] => { const { fromRange, toRange } = params.params || {}; if (!toRange || !fromRange) { @@ -499,6 +526,32 @@ export const handleIRemoveRow = (param: IRemoveRowColCommand, targetRange: IRang } return operators; }; + +export const handleReorderRange = (param: IReorderRangeCommand, targetRange: IRange) => { + const { range, order } = param.params || {}; + if (!range || !order) { + return []; + } + + if (Rectangle.contains(range, targetRange) && targetRange.endRow === targetRange.startRow) { + const operators: IOperator[] = []; + const targetRow = targetRange.startRow; + for (const k in order) { + if (order[k] === targetRow) { + const toRow = Number(k); + operators.push({ + type: OperatorType.VerticalMove, + step: toRow - targetRow, + length: 0, + }); + return operators; + } + } + return []; + } + return []; +}; + // see docs/tldr/ref-range/insert-rows-cols.tldr export const handleBaseInsertRange = (_insertRange: IRange, _targetRange: IRange) => { const insertRange = handleRangeTypeInput(_insertRange); @@ -904,6 +957,10 @@ export const handleDefaultRangeChangeWithEffectRefCommands = (range: IRange, com operator = handleIRemoveRow(commandInfo as IRemoveRowColCommand, range); break; } + case EffectRefRangId.ReorderRangeCommandId: { + operator = handleReorderRange(commandInfo as IReorderRangeCommand, range); + break; + } } const resultRange = runRefRangeMutations(operator, range); @@ -962,6 +1019,9 @@ export const handleCommonDefaultRangeChangeWithEffectRefCommands = (range: IRang case EffectRefRangId.MoveRowsCommandId: { return handleMoveRowsCommon(commandInfo as IMoveRowsCommand, range); } + case EffectRefRangId.ReorderRangeCommandId: { + return handleReorderRangeCommon(commandInfo as IReorderRangeCommand, range); + } case EffectRefRangId.RemoveColCommandId: { operator = handleIRemoveCol(commandInfo as IRemoveRowColCommand, range); break; @@ -1136,6 +1196,22 @@ export function getEffectedRangesOnCommand(command: EffectRefRangeParams, deps: } return [range]; } + case EffectRefRangId.ReorderRangeCommandId: { + const params = command; + const { range, order } = params.params!; + const effectRanges = []; + for (let row = range.startRow; row <= range.endRow; row++) { + if (row in order) { + effectRanges.push({ + startRow: row, + endRow: row, + startColumn: range.startColumn, + endColumn: range.endColumn, + }); + } + } + return effectRanges; + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 272a4afef1..7d2408005f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,6 +288,12 @@ importers: '@univerjs/sheets-numfmt': specifier: workspace:* version: link:../packages/sheets-numfmt + '@univerjs/sheets-sort': + specifier: workspace:* + version: link:../packages/sheets-sort + '@univerjs/sheets-sort-ui': + specifier: workspace:* + version: link:../packages/sheets-sort-ui '@univerjs/sheets-thread-comment': specifier: workspace:* version: link:../packages/sheets-thread-comment @@ -1777,6 +1783,107 @@ importers: specifier: ^1.6.0 version: 1.6.0(@types/node@20.14.4)(happy-dom@13.3.8)(jsdom@24.1.0)(less@4.2.0)(sass@1.77.5)(terser@5.31.1) + packages/sheets-sort: + dependencies: + '@univerjs/engine-formula': + specifier: workspace:* + version: link:../engine-formula + '@univerjs/sheets-ui': + specifier: workspace:* + version: link:../sheets-ui + '@univerjs/ui': + specifier: workspace:* + version: link:../ui + devDependencies: + '@univerjs/core': + specifier: workspace:* + version: link:../core + '@univerjs/shared': + specifier: workspace:* + version: link:../../common/shared + '@univerjs/sheets': + specifier: workspace:* + version: link:../sheets + '@wendellhu/redi': + specifier: ^0.15.2 + version: 0.15.4 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + vite: + specifier: ^5.2.10 + version: 5.3.1(@types/node@20.14.4)(less@4.2.0)(sass@1.77.5)(terser@5.31.1) + vitest: + specifier: ^1.5.2 + version: 1.6.0(@types/node@20.14.4)(happy-dom@13.3.8)(jsdom@24.1.0)(less@4.2.0)(sass@1.77.5)(terser@5.31.1) + + packages/sheets-sort-ui: + dependencies: + '@univerjs/engine-formula': + specifier: workspace:* + version: link:../engine-formula + '@univerjs/icons': + specifier: ^0.1.56 + version: 0.1.57(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: + specifier: ^3.11.5 + version: 3.14.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@univerjs/core': + specifier: workspace:* + version: link:../core + '@univerjs/design': + specifier: workspace:* + version: link:../design + '@univerjs/engine-render': + specifier: workspace:* + version: link:../engine-render + '@univerjs/shared': + specifier: workspace:* + version: link:../../common/shared + '@univerjs/sheets': + specifier: workspace:* + version: link:../sheets + '@univerjs/sheets-sort': + specifier: workspace:* + version: link:../sheets-sort + '@univerjs/sheets-ui': + specifier: workspace:* + version: link:../sheets-ui + '@univerjs/ui': + specifier: workspace:* + version: link:../ui + '@wendellhu/redi': + specifier: ^0.15.2 + version: 0.15.4 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + less: + specifier: ^4.2.0 + version: 4.2.0 + react: + specifier: 18.3.1 + version: 18.3.1 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + vite: + specifier: ^5.2.10 + version: 5.3.1(@types/node@20.14.4)(less@4.2.0)(sass@1.77.5)(terser@5.31.1) + vitest: + specifier: ^1.5.2 + version: 1.6.0(@types/node@20.14.4)(happy-dom@13.3.8)(jsdom@24.1.0)(less@4.2.0)(sass@1.77.5)(terser@5.31.1) + packages/sheets-thread-comment: dependencies: '@univerjs/engine-render': @@ -3927,7 +4034,7 @@ packages: resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + resolution: {integrity: sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=} '@protobufjs/inquire@1.1.0': resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}